From cf1d39643d3c0d7d56379eba9a1f5c6368d0bc12 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 12 Jul 2023 15:44:05 +0200 Subject: [PATCH 001/238] SOP-Java 7.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index ed3e547..d8ddfd6 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '7.0.0' - isSnapshot = false + shortVersion = '7.0.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 4bd46579068977ef12efc8ac8cc65ed73c8be747 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 11:24:27 +0100 Subject: [PATCH 002/238] Add kotlin plugin --- build.gradle | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/build.gradle b/build.gradle index 56730fd..577c2aa 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,8 @@ buildscript { plugins { id 'ru.vyarus.animalsniffer' version '1.5.3' + id 'org.jetbrains.kotlin.jvm' version "1.8.10" + id 'com.diffplug.spotless' version '6.22.0' apply false } apply from: 'version.gradle' @@ -29,6 +31,8 @@ allprojects { apply plugin: 'eclipse' apply plugin: 'jacoco' apply plugin: 'checkstyle' + apply plugin: 'kotlin' + apply plugin: 'com.diffplug.spotless' // For non-cli modules enable android api compatibility check if (it.name.equals('sop-java')) { @@ -53,6 +57,12 @@ allprojects { toolVersion = '8.18' } + spotless { + kotlin { + ktfmt().dropboxStyle() + } + } + group 'org.pgpainless' description = "Stateless OpenPGP Protocol API for Java" version = shortVersion @@ -69,6 +79,13 @@ allprojects { reproducibleFileOrder = true } + // Compatibility of default implementations in kotlin interfaces with Java implementations. + tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { + kotlinOptions { + freeCompilerArgs += ["-Xjvm-default=all-compatibility"] + } + } + project.ext { rootConfigDir = new File(rootDir, 'config') gitCommit = getGitCommit() From 0f5270c28da4f63c364a9f1d2561355a0f2f6fa9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 11:24:36 +0100 Subject: [PATCH 003/238] Kotlin conversion: ByteArrayAndResult --- .../src/main/java/sop/ByteArrayAndResult.java | 50 ------------------- .../src/main/kotlin/sop/ByteArrayAndResult.kt | 43 ++++++++++++++++ 2 files changed, 43 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/ByteArrayAndResult.java create mode 100644 sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt diff --git a/sop-java/src/main/java/sop/ByteArrayAndResult.java b/sop-java/src/main/java/sop/ByteArrayAndResult.java deleted file mode 100644 index fd2b39a..0000000 --- a/sop-java/src/main/java/sop/ByteArrayAndResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -/** - * Tuple of a byte array and associated result object. - * @param type of result - */ -public class ByteArrayAndResult { - - private final byte[] bytes; - private final T result; - - public ByteArrayAndResult(byte[] bytes, T result) { - this.bytes = bytes; - this.result = result; - } - - /** - * Return the byte array part. - * - * @return bytes - */ - public byte[] getBytes() { - return bytes; - } - - /** - * Return the result part. - * - * @return result - */ - public T getResult() { - return result; - } - - /** - * Return the byte array part as an {@link InputStream}. - * - * @return input stream - */ - public InputStream getInputStream() { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt b/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt new file mode 100644 index 0000000..021a2d9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/ByteArrayAndResult.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.InputStream + +/** + * Tuple of a [ByteArray] and associated result object. + * + * @param bytes byte array + * @param result result object + * @param type of result + */ +data class ByteArrayAndResult(val bytes: ByteArray, val result: T) { + + /** + * [InputStream] returning the contents of [bytes]. + * + * @return input stream + */ + val inputStream: InputStream + get() = bytes.inputStream() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as ByteArrayAndResult<*> + + if (!bytes.contentEquals(other.bytes)) return false + if (result != other.result) return false + + return true + } + + override fun hashCode(): Int { + var hashCode = bytes.contentHashCode() + hashCode = 31 * hashCode + (result?.hashCode() ?: 0) + return hashCode + } +} From 567571cf6cbc657e6f4713dea38bda7f34eb63ac Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 11:33:15 +0100 Subject: [PATCH 004/238] Kotlin conversion: SessionKey --- sop-java/src/main/java/sop/SessionKey.java | 80 ---------------------- sop-java/src/main/kotlin/sop/SessionKey.kt | 48 +++++++++++++ 2 files changed, 48 insertions(+), 80 deletions(-) delete mode 100644 sop-java/src/main/java/sop/SessionKey.java create mode 100644 sop-java/src/main/kotlin/sop/SessionKey.kt diff --git a/sop-java/src/main/java/sop/SessionKey.java b/sop-java/src/main/java/sop/SessionKey.java deleted file mode 100644 index 722666d..0000000 --- a/sop-java/src/main/java/sop/SessionKey.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.Arrays; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import sop.util.HexUtil; - -public class SessionKey { - - private static final Pattern PATTERN = Pattern.compile("^(\\d):([0-9A-F]+)$"); - - private final byte algorithm; - private final byte[] sessionKey; - - public SessionKey(byte algorithm, byte[] sessionKey) { - this.algorithm = algorithm; - this.sessionKey = sessionKey; - } - - /** - * Return the symmetric algorithm octet. - * - * @return algorithm id - */ - public byte getAlgorithm() { - return algorithm; - } - - /** - * Return the session key. - * - * @return session key - */ - public byte[] getKey() { - return sessionKey; - } - - @Override - public int hashCode() { - return getAlgorithm() * 17 + Arrays.hashCode(getKey()); - } - - @Override - public boolean equals(Object other) { - if (other == null) { - return false; - } - if (this == other) { - return true; - } - if (!(other instanceof SessionKey)) { - return false; - } - - SessionKey otherKey = (SessionKey) other; - return getAlgorithm() == otherKey.getAlgorithm() && Arrays.equals(getKey(), otherKey.getKey()); - } - - public static SessionKey fromString(String string) { - string = string.trim().toUpperCase().replace("\n", ""); - Matcher matcher = PATTERN.matcher(string); - if (!matcher.matches()) { - throw new IllegalArgumentException("Provided session key does not match expected format."); - } - byte algorithm = Byte.parseByte(matcher.group(1)); - String key = matcher.group(2); - - return new SessionKey(algorithm, HexUtil.hexToBytes(key)); - } - - @Override - public String toString() { - return Integer.toString(getAlgorithm()) + ':' + HexUtil.bytesToHex(sessionKey); - } -} diff --git a/sop-java/src/main/kotlin/sop/SessionKey.kt b/sop-java/src/main/kotlin/sop/SessionKey.kt new file mode 100644 index 0000000..af230f3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SessionKey.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.HexUtil + +/** + * Class representing a symmetric session key. + * + * @param algorithm symmetric key algorithm ID + * @param key [ByteArray] containing the session key + */ +data class SessionKey(val algorithm: Byte, val key: ByteArray) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as SessionKey + + if (algorithm != other.algorithm) return false + if (!key.contentEquals(other.key)) return false + + return true + } + + override fun hashCode(): Int { + var hashCode = algorithm.toInt() + hashCode = 31 * hashCode + key.contentHashCode() + return hashCode + } + + override fun toString(): String = "$algorithm:${HexUtil.bytesToHex(key)}" + + companion object { + + @JvmStatic private val PATTERN = "^(\\d):([0-9A-F]+)$".toPattern() + + @JvmStatic + fun fromString(string: String): SessionKey { + val matcher = PATTERN.matcher(string.trim().uppercase().replace("\n", "")) + require(matcher.matches()) { "Provided session key does not match expected format." } + return SessionKey(matcher.group(1).toByte(), HexUtil.hexToBytes(matcher.group(2))) + } + } +} From 6c5c4b3d981aa62f28dfde36a2e56821d279620f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:25:54 +0100 Subject: [PATCH 005/238] Kotlin conversion: Verification --- sop-java/src/main/java/sop/Verification.java | 222 ------------------- sop-java/src/main/kotlin/sop/Verification.kt | 76 +++++++ 2 files changed, 76 insertions(+), 222 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Verification.java create mode 100644 sop-java/src/main/kotlin/sop/Verification.kt diff --git a/sop-java/src/main/java/sop/Verification.java b/sop-java/src/main/java/sop/Verification.java deleted file mode 100644 index 140e23b..0000000 --- a/sop-java/src/main/java/sop/Verification.java +++ /dev/null @@ -1,222 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.enums.SignatureMode; -import sop.util.Optional; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import java.text.ParseException; -import java.util.Date; - -/** - * Class bundling information about a verified signature. - */ -public class Verification { - - private final Date creationTime; - private final String signingKeyFingerprint; - private final String signingCertFingerprint; - private final Optional signatureMode; - private final Optional description; - - private static final String MODE = "mode:"; - - /** - * Create a new {@link Verification} without mode and description. - * - * @param creationTime signature creation time - * @param signingKeyFingerprint fingerprint of the signing (sub-) key - * @param signingCertFingerprint fingerprint of the certificate - */ - public Verification(@Nonnull Date creationTime, - @Nonnull String signingKeyFingerprint, - @Nonnull String signingCertFingerprint) { - this(creationTime, signingKeyFingerprint, signingCertFingerprint, Optional.ofEmpty(), Optional.ofEmpty()); - } - - /** - * Create a new {@link Verification}. - * - * @param creationTime signature creation time - * @param signingKeyFingerprint fingerprint of the signing (sub-) key - * @param signingCertFingerprint fingerprint of the certificate - * @param signatureMode signature mode (optional, may be
null
) - * @param description free-form description, e.g.
certificate from dkg.asc
(optional, may be
null
) - */ - public Verification(@Nonnull Date creationTime, - @Nonnull String signingKeyFingerprint, - @Nonnull String signingCertFingerprint, - @Nullable SignatureMode signatureMode, - @Nullable String description) { - this( - creationTime, - signingKeyFingerprint, - signingCertFingerprint, - Optional.ofNullable(signatureMode), - Optional.ofNullable(nullSafeTrim(description)) - ); - } - - private Verification(@Nonnull Date creationTime, - @Nonnull String signingKeyFingerprint, - @Nonnull String signingCertFingerprint, - @Nonnull Optional signatureMode, - @Nonnull Optional description) { - this.creationTime = creationTime; - this.signingKeyFingerprint = signingKeyFingerprint; - this.signingCertFingerprint = signingCertFingerprint; - this.signatureMode = signatureMode; - this.description = description; - } - - private static String nullSafeTrim(@Nullable String string) { - if (string == null) { - return null; - } - return string.trim(); - } - - @Nonnull - public static Verification fromString(@Nonnull String toString) { - String[] split = toString.trim().split(" "); - if (split.length < 3) { - throw new IllegalArgumentException("Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'"); - } - - if (split.length == 3) { - return new Verification( - parseUTCDate(split[0]), // timestamp - split[1], // key FP - split[2] // cert FP - ); - } - - SignatureMode mode = null; - int index = 3; - if (split[index].startsWith(MODE)) { - mode = SignatureMode.valueOf(split[3].substring(MODE.length())); - index++; - } - - StringBuilder sb = new StringBuilder(); - for (int i = index; i < split.length; i++) { - if (sb.length() != 0) { - sb.append(' '); - } - sb.append(split[i]); - } - - return new Verification( - parseUTCDate(split[0]), // timestamp - split[1], // key FP - split[2], // cert FP - mode, // signature mode - sb.length() != 0 ? sb.toString() : null // description - ); - } - - private static Date parseUTCDate(String utcFormatted) { - try { - return UTCUtil.parseUTCDate(utcFormatted); - } catch (ParseException e) { - throw new IllegalArgumentException("Malformed UTC timestamp.", e); - } - } - - /** - * Return the signatures' creation time. - * - * @return signature creation time - */ - @Nonnull - public Date getCreationTime() { - return creationTime; - } - - /** - * Return the fingerprint of the signing (sub)key. - * - * @return signing key fingerprint - */ - @Nonnull - public String getSigningKeyFingerprint() { - return signingKeyFingerprint; - } - - /** - * Return the fingerprint fo the signing certificate. - * - * @return signing certificate fingerprint - */ - @Nonnull - public String getSigningCertFingerprint() { - return signingCertFingerprint; - } - - /** - * Return the mode of the signature. - * Optional, may return
null
. - * - * @return signature mode - */ - @Nonnull - public Optional getSignatureMode() { - return signatureMode; - } - - /** - * Return an optional description. - * Optional, may return
null
. - * - * @return description - */ - @Nonnull - public Optional getDescription() { - return description; - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder() - .append(UTCUtil.formatUTCDate(getCreationTime())) - .append(' ') - .append(getSigningKeyFingerprint()) - .append(' ') - .append(getSigningCertFingerprint()); - - if (signatureMode.isPresent()) { - sb.append(' ').append(MODE).append(signatureMode.get()); - } - - if (description.isPresent()) { - sb.append(' ').append(description.get()); - } - - return sb.toString(); - } - - @Override - public int hashCode() { - return toString().hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj == null) { - return false; - } - if (this == obj) { - return true; - } - if (!(obj instanceof Verification)) { - return false; - } - Verification other = (Verification) obj; - return toString().equals(other.toString()); - } -} diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt new file mode 100644 index 0000000..20401a9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.text.ParseException +import java.util.Date +import sop.enums.SignatureMode +import sop.util.Optional +import sop.util.UTCUtil + +data class Verification( + val creationTime: Date, + val signingKeyFingerprint: String, + val signingCertFingerprint: String, + val signatureMode: Optional, + val description: Optional +) { + @JvmOverloads + constructor( + creationTime: Date, + signingKeyFingerprint: String, + signingCertFingerprint: String, + signatureMode: SignatureMode? = null, + description: String? = null + ) : this( + creationTime, + signingKeyFingerprint, + signingCertFingerprint, + Optional.ofNullable(signatureMode), + Optional.ofNullable(description?.trim())) + + override fun toString(): String = + "${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" + + (if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") + + (if (description.isPresent) " ${description.get()}" else "") + + companion object { + @JvmStatic + fun fromString(string: String): Verification { + val split = string.trim().split(" ") + require(split.size >= 3) { + "Verification must be of the format 'UTC-DATE OpenPGPFingerprint OpenPGPFingerprint [mode] [info]'." + } + if (split.size == 3) { + return Verification(parseUTCDate(split[0]), split[1], split[2]) + } + + var index = 3 + val mode = + if (split[3].startsWith("mode:")) { + index += 1 + SignatureMode.valueOf(split[3].substring("mode:".length)) + } else null + + val description = split.subList(index, split.size).joinToString(" ").ifBlank { null } + + return Verification( + parseUTCDate(split[0]), + split[1], + split[2], + Optional.ofNullable(mode), + Optional.ofNullable(description)) + } + + @JvmStatic + private fun parseUTCDate(string: String): Date { + return try { + UTCUtil.parseUTCDate(string) + } catch (e: ParseException) { + throw IllegalArgumentException("Malformed UTC timestamp.", e) + } + } + } +} From ef4b01c6bd73d74a3b3d203ec5da2a0228431f4e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:42:13 +0100 Subject: [PATCH 006/238] Kotlin conversion: Profile --- sop-java/src/main/java/sop/Profile.java | 143 ------------------------ sop-java/src/main/kotlin/sop/Profile.kt | 87 ++++++++++++++ 2 files changed, 87 insertions(+), 143 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Profile.java create mode 100644 sop-java/src/main/kotlin/sop/Profile.kt diff --git a/sop-java/src/main/java/sop/Profile.java b/sop-java/src/main/java/sop/Profile.java deleted file mode 100644 index 4ea9e71..0000000 --- a/sop-java/src/main/java/sop/Profile.java +++ /dev/null @@ -1,143 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.util.Optional; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; - -/** - * Tuple class bundling a profile name and description. - * - * @see - * SOP Spec - Profile - */ -public class Profile { - - private final String name; - private final Optional description; - - /** - * Create a new {@link Profile} object. - * The {@link #toString()} representation MUST NOT exceed a length of 1000 bytes. - * - * @param name profile name - * @param description profile description - */ - public Profile(@Nonnull String name, @Nullable String description) { - if (name.trim().isEmpty()) { - throw new IllegalArgumentException("Name cannot be empty."); - } - if (name.contains(":")) { - throw new IllegalArgumentException("Name cannot contain ':'."); - } - if (name.contains(" ") || name.contains("\n") || name.contains("\t") || name.contains("\r")) { - throw new IllegalArgumentException("Name cannot contain whitespace characters."); - } - - this.name = name; - - if (description == null) { - this.description = Optional.ofEmpty(); - } else { - String trimmedDescription = description.trim(); - if (trimmedDescription.isEmpty()) { - this.description = Optional.ofEmpty(); - } else { - this.description = Optional.of(trimmedDescription); - } - } - - if (exceeds1000CharLineLimit(this)) { - throw new IllegalArgumentException("The line representation of a profile MUST NOT exceed 1000 bytes."); - } - } - - public Profile(String name) { - this(name, null); - } - - /** - * Parse a {@link Profile} from its string representation. - * - * @param string string representation - * @return profile - */ - public static Profile parse(String string) { - if (string.contains(": ")) { - // description after colon, e.g. "default: Use implementers recommendations." - String name = string.substring(0, string.indexOf(": ")); - String description = string.substring(string.indexOf(": ") + 2); - return new Profile(name, description.trim()); - } - - if (string.endsWith(":")) { - // empty description, e.g. "default:" - return new Profile(string.substring(0, string.length() - 1)); - } - - // no description - return new Profile(string.trim()); - } - - /** - * Return the name (also known as identifier) of the profile. - * A profile name is a UTF-8 string that has no whitespace in it. - * Similar to OpenPGP Notation names, profile names are divided into two namespaces: - * The IETF namespace and the user namespace. - * A profile name in the user namespace ends with the
@
character (0x40) followed by a DNS domain name. - * A profile name in the IETF namespace does not have an
@
character. - * A profile name in the user space is owned and controlled by the owner of the domain in the suffix. - * A profile name in the IETF namespace that begins with the string
rfc
should have semantics that hew as - * closely as possible to the referenced RFC. - * Similarly, a profile name in the IETF namespace that begins with the string
draft-
should have - * semantics that hew as closely as possible to the referenced Internet Draft. - * - * @return name - */ - @Nonnull - public String getName() { - return name; - } - - /** - * Return a free-form description of the profile. - * - * @return description - */ - @Nonnull - public Optional getDescription() { - return description; - } - - public boolean hasDescription() { - return description.isPresent(); - } - - /** - * Convert the profile into a String for displaying. - * - * @return string - */ - @Override - public String toString() { - if (getDescription().isEmpty()) { - return getName(); - } - return getName() + ": " + getDescription().get(); - } - - /** - * Test if the string representation of the profile exceeds the limit of 1000 bytes length. - * @param profile profile - * @return
true
if the profile exceeds 1000 bytes,
false
otherwise. - */ - private static boolean exceeds1000CharLineLimit(Profile profile) { - String line = profile.toString(); - return line.getBytes(UTF8Util.UTF8).length > 1000; - } -} diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt new file mode 100644 index 0000000..2125c57 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional +import sop.util.UTF8Util + +/** + * Tuple class bundling a profile name and description. + * + * @param name profile name. A profile name is a UTF-8 string that has no whitespace in it. Similar + * to OpenPGP Notation names, profile names are divided into two namespaces: The IETF namespace + * and the user namespace. A profile name in the user namespace ends with the `@` character (0x40) + * followed by a DNS domain name. A profile name in the IETF namespace does not have an `@` + * character. A profile name in the user space is owned and controlled by the owner of the domain + * in the suffix. A profile name in the IETF namespace that begins with the string `rfc` should + * have semantics that hew as closely as possible to the referenced RFC. Similarly, a profile name + * in the IETF namespace that begins with the string `draft-` should have semantics that hew as + * closely as possible to the referenced Internet Draft. + * @param description a free-form description of the profile. + * @see + * SOP Spec - Profile + */ +data class Profile(val name: String, val description: Optional) { + + @JvmOverloads + constructor( + name: String, + description: String? = null + ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null })) + + init { + require(name.trim().isNotBlank()) { "Name cannot be empty." } + require(!name.contains(":")) { "Name cannot contain ':'." } + require(listOf(" ", "\n", "\t", "\r").none { name.contains(it) }) { + "Name cannot contain whitespace characters." + } + require(!exceeds1000CharLineLimit(this)) { + "The line representation of a profile MUST NOT exceed 1000 bytes." + } + } + + fun hasDescription() = description.isPresent + + /** + * Convert the profile into a String for displaying. + * + * @return string + */ + override fun toString(): String = + if (description.isEmpty) name else "$name: ${description.get()}" + + companion object { + + /** + * Parse a [Profile] from its string representation. + * + * @param string string representation + * @return profile + */ + @JvmStatic + fun parse(string: String): Profile { + return if (string.contains(": ")) { + Profile( + string.substring(0, string.indexOf(": ")), + string.substring(string.indexOf(": ") + 2).trim()) + } else if (string.endsWith(":")) { + Profile(string.substring(0, string.length - 1)) + } else { + Profile(string.trim()) + } + } + + /** + * Test if the string representation of the profile exceeds the limit of 1000 bytes length. + * + * @param profile profile + * @return `true` if the profile exceeds 1000 bytes, `false` otherwise. + */ + @JvmStatic + private fun exceeds1000CharLineLimit(profile: Profile): Boolean = + profile.toString().toByteArray(UTF8Util.UTF8).size > 1000 + } +} From 0cb5c74a11109cbe7a3b266a95f7655b8ef68441 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:48:23 +0100 Subject: [PATCH 007/238] Kotlin conversion: Optional --- sop-java/src/main/java/sop/util/Optional.java | 50 ------------------- sop-java/src/main/kotlin/sop/util/Optional.kt | 26 ++++++++++ 2 files changed, 26 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/Optional.java create mode 100644 sop-java/src/main/kotlin/sop/util/Optional.kt diff --git a/sop-java/src/main/java/sop/util/Optional.java b/sop-java/src/main/java/sop/util/Optional.java deleted file mode 100644 index 00eb201..0000000 --- a/sop-java/src/main/java/sop/util/Optional.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -/** - * Backport of java.util.Optional for older Android versions. - * - * @param item type - */ -public class Optional { - - private final T item; - - public Optional() { - this(null); - } - - public Optional(T item) { - this.item = item; - } - - public static Optional of(T item) { - if (item == null) { - throw new NullPointerException("Item cannot be null."); - } - return new Optional<>(item); - } - - public static Optional ofNullable(T item) { - return new Optional<>(item); - } - - public static Optional ofEmpty() { - return new Optional<>(null); - } - - public T get() { - return item; - } - - public boolean isPresent() { - return item != null; - } - - public boolean isEmpty() { - return item == null; - } -} diff --git a/sop-java/src/main/kotlin/sop/util/Optional.kt b/sop-java/src/main/kotlin/sop/util/Optional.kt new file mode 100644 index 0000000..0344d0b --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/Optional.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +/** + * Backport of java.util.Optional for older Android versions. + * + * @param item type + */ +data class Optional(val item: T? = null) { + + val isPresent: Boolean = item != null + val isEmpty: Boolean = item == null + + fun get() = item + + companion object { + @JvmStatic fun of(item: T) = Optional(item!!) + + @JvmStatic fun ofNullable(item: T?) = Optional(item) + + @JvmStatic fun ofEmpty() = Optional(null as T?) + } +} From bbe159e88c5442a61ae5dcbe7fca695f408cf785 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 12:56:12 +0100 Subject: [PATCH 008/238] Kotlin conversion: SigningResult --- sop-java/src/main/java/sop/SigningResult.java | 50 ------------------- sop-java/src/main/kotlin/sop/SigningResult.kt | 28 +++++++++++ 2 files changed, 28 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/SigningResult.java create mode 100644 sop-java/src/main/kotlin/sop/SigningResult.kt diff --git a/sop-java/src/main/java/sop/SigningResult.java b/sop-java/src/main/java/sop/SigningResult.java deleted file mode 100644 index 1ea1ba8..0000000 --- a/sop-java/src/main/java/sop/SigningResult.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -/** - * This class contains various information about a signed message. - */ -public final class SigningResult { - - private final MicAlg micAlg; - - private SigningResult(MicAlg micAlg) { - this.micAlg = micAlg; - } - - /** - * Return a string identifying the digest mechanism used to create the signed message. - * This is useful for setting the micalg= parameter for the multipart/signed - * content type of a PGP/MIME object as described in section 5 of [RFC3156]. - *

- * If more than one signature was generated and different digest mechanisms were used, - * the value of the micalg object is an empty string. - * - * @return micalg - */ - public MicAlg getMicAlg() { - return micAlg; - } - - public static Builder builder() { - return new Builder(); - } - - public static class Builder { - - private MicAlg micAlg; - - public Builder setMicAlg(MicAlg micAlg) { - this.micAlg = micAlg; - return this; - } - - public SigningResult build() { - SigningResult signingResult = new SigningResult(micAlg); - return signingResult; - } - } -} diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt new file mode 100644 index 0000000..29304ea --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SigningResult.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +/** + * This class contains various information about a signed message. + * + * @param micAlg string identifying the digest mechanism used to create the signed message. This is + * useful for setting the `micalg=` parameter for the multipart/signed content-type of a PGP/MIME + * object as described in section 5 of [RFC3156]. If more than one signature was generated and + * different digest mechanisms were used, the value of the micalg object is an empty string. + */ +data class SigningResult(val micAlg: MicAlg) { + + class Builder internal constructor() { + private var micAlg = MicAlg.empty() + + fun setMicAlg(micAlg: MicAlg) = apply { this.micAlg = micAlg } + + fun build() = SigningResult(micAlg) + } + + companion object { + @JvmStatic fun builder() = Builder() + } +} From 9dbb93e13dd14083d48622b2ff2594db41ec9de8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:05:30 +0100 Subject: [PATCH 009/238] Kotlin conversion: MicAlg --- sop-java/src/main/java/sop/MicAlg.java | 55 ---------------------- sop-java/src/main/kotlin/sop/MicAlg.kt | 34 +++++++++++++ sop-java/src/test/java/sop/MicAlgTest.java | 2 +- 3 files changed, 35 insertions(+), 56 deletions(-) delete mode 100644 sop-java/src/main/java/sop/MicAlg.java create mode 100644 sop-java/src/main/kotlin/sop/MicAlg.kt diff --git a/sop-java/src/main/java/sop/MicAlg.java b/sop-java/src/main/java/sop/MicAlg.java deleted file mode 100644 index 5bee787..0000000 --- a/sop-java/src/main/java/sop/MicAlg.java +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.OutputStream; -import java.io.PrintWriter; - -public class MicAlg { - - private final String micAlg; - - public MicAlg(String micAlg) { - if (micAlg == null) { - throw new IllegalArgumentException("MicAlg String cannot be null."); - } - this.micAlg = micAlg; - } - - public static MicAlg empty() { - return new MicAlg(""); - } - - public static MicAlg fromHashAlgorithmId(int id) { - switch (id) { - case 1: - return new MicAlg("pgp-md5"); - case 2: - return new MicAlg("pgp-sha1"); - case 3: - return new MicAlg("pgp-ripemd160"); - case 8: - return new MicAlg("pgp-sha256"); - case 9: - return new MicAlg("pgp-sha384"); - case 10: - return new MicAlg("pgp-sha512"); - case 11: - return new MicAlg("pgp-sha224"); - default: - throw new IllegalArgumentException("Unsupported hash algorithm ID: " + id); - } - } - - public String getMicAlg() { - return micAlg; - } - - public void writeTo(OutputStream outputStream) { - PrintWriter pw = new PrintWriter(outputStream); - pw.write(getMicAlg()); - pw.close(); - } -} diff --git a/sop-java/src/main/kotlin/sop/MicAlg.kt b/sop-java/src/main/kotlin/sop/MicAlg.kt new file mode 100644 index 0000000..58ce7b5 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/MicAlg.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.OutputStream +import java.io.PrintWriter + +data class MicAlg(val micAlg: String) { + + fun writeTo(outputStream: OutputStream) { + PrintWriter(outputStream).use { it.write(micAlg) } + } + + companion object { + @JvmStatic fun empty() = MicAlg("") + + @JvmStatic + fun fromHashAlgorithmId(id: Int) = + when (id) { + 1 -> "pgp-md5" + 2 -> "pgp-sha1" + 3 -> "pgp-ripemd160" + 8 -> "pgp-sha256" + 9 -> "pgp-sha384" + 10 -> "pgp-sha512" + 11 -> "pgp-sha224" + 12 -> "pgp-sha3-256" + 14 -> "pgp-sha3-512" + else -> throw IllegalArgumentException("Unsupported hash algorithm ID: $id") + }.let { MicAlg(it) } + } +} diff --git a/sop-java/src/test/java/sop/MicAlgTest.java b/sop-java/src/test/java/sop/MicAlgTest.java index 16f54ef..7c6b30a 100644 --- a/sop-java/src/test/java/sop/MicAlgTest.java +++ b/sop-java/src/test/java/sop/MicAlgTest.java @@ -18,7 +18,7 @@ public class MicAlgTest { @Test public void constructorNullArgThrows() { - assertThrows(IllegalArgumentException.class, () -> new MicAlg(null)); + assertThrows(NullPointerException.class, () -> new MicAlg(null)); } @Test From e6562cecff7e8c444f8d410101a6a2c70378399f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:16:37 +0100 Subject: [PATCH 010/238] Kotlin conversion: Ready --- sop-java/src/main/java/sop/Ready.java | 45 ------------------------ sop-java/src/main/kotlin/sop/Ready.kt | 49 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+), 45 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Ready.java create mode 100644 sop-java/src/main/kotlin/sop/Ready.kt diff --git a/sop-java/src/main/java/sop/Ready.java b/sop-java/src/main/java/sop/Ready.java deleted file mode 100644 index 71ab26e..0000000 --- a/sop-java/src/main/java/sop/Ready.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -public abstract class Ready { - - /** - * Write the data to the provided output stream. - * - * @param outputStream output stream - * @throws IOException in case of an IO error - */ - public abstract void writeTo(OutputStream outputStream) throws IOException; - - /** - * Return the data as a byte array by writing it to a {@link ByteArrayOutputStream} first and then returning - * the array. - * - * @return data as byte array - * @throws IOException in case of an IO error - */ - public byte[] getBytes() throws IOException { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - writeTo(bytes); - return bytes.toByteArray(); - } - - /** - * Return an input stream containing the data. - * - * @return input stream - * @throws IOException in case of an IO error - */ - public InputStream getInputStream() throws IOException { - return new ByteArrayInputStream(getBytes()); - } -} diff --git a/sop-java/src/main/kotlin/sop/Ready.kt b/sop-java/src/main/kotlin/sop/Ready.kt new file mode 100644 index 0000000..1eb3fb3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Ready.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** Abstract class that encapsulates output data, waiting to be consumed. */ +abstract class Ready { + + /** + * Write the data to the provided output stream. + * + * @param outputStream output stream + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) abstract fun writeTo(outputStream: OutputStream) + + /** + * Return the data as a byte array by writing it to a [ByteArrayOutputStream] first and then + * returning the array. + * + * @return data as byte array + * @throws IOException in case of an IO error + */ + val bytes: ByteArray + @Throws(IOException::class) + get() = + ByteArrayOutputStream() + .let { + writeTo(it) + it + } + .toByteArray() + + /** + * Return an input stream containing the data. + * + * @return input stream + * @throws IOException in case of an IO error + */ + val inputStream: InputStream + @Throws(IOException::class) get() = ByteArrayInputStream(bytes) +} From a89e70c19e63362d52b0946aeacad73e6c1bfdc9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:23:26 +0100 Subject: [PATCH 011/238] Kotlin conversion: ReadyWithResult --- .../src/main/java/sop/ReadyWithResult.java | 41 ------------------- .../src/main/kotlin/sop/ReadyWithResult.kt | 41 +++++++++++++++++++ 2 files changed, 41 insertions(+), 41 deletions(-) delete mode 100644 sop-java/src/main/java/sop/ReadyWithResult.java create mode 100644 sop-java/src/main/kotlin/sop/ReadyWithResult.kt diff --git a/sop-java/src/main/java/sop/ReadyWithResult.java b/sop-java/src/main/java/sop/ReadyWithResult.java deleted file mode 100644 index 9feedda..0000000 --- a/sop-java/src/main/java/sop/ReadyWithResult.java +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -import sop.exception.SOPGPException; - -public abstract class ReadyWithResult { - - /** - * Write the data e.g. decrypted plaintext to the provided output stream and return the result of the - * processing operation. - * - * @param outputStream output stream - * @return result, eg. signatures - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public abstract T writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature; - - /** - * Return the data as a {@link ByteArrayAndResult}. - * Calling {@link ByteArrayAndResult#getBytes()} will give you access to the data as byte array, while - * {@link ByteArrayAndResult#getResult()} will grant access to the appended result. - * - * @return byte array and result - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature if there are no valid signatures found - */ - public ByteArrayAndResult toByteArrayAndResult() throws IOException, SOPGPException.NoSignature { - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - T result = writeTo(bytes); - return new ByteArrayAndResult<>(bytes.toByteArray(), result); - } -} diff --git a/sop-java/src/main/kotlin/sop/ReadyWithResult.kt b/sop-java/src/main/kotlin/sop/ReadyWithResult.kt new file mode 100644 index 0000000..d309c76 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/ReadyWithResult.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream +import sop.exception.SOPGPException + +abstract class ReadyWithResult { + + /** + * Write the data e.g. decrypted plaintext to the provided output stream and return the result + * of the processing operation. + * + * @param outputStream output stream + * @return result, eg. signatures + * @throws IOException in case of an IO error + * @throws SOPGPException in case of a SOP protocol error + */ + @Throws(IOException::class, SOPGPException::class) + abstract fun writeTo(outputStream: OutputStream): T + + /** + * Return the data as a [ByteArrayAndResult]. Calling [ByteArrayAndResult.bytes] will give you + * access to the data as byte array, while [ByteArrayAndResult.result] will grant access to the + * appended result. + * + * @return byte array and result + * @throws IOException in case of an IO error + * @throws SOPGPException.NoSignature if there are no valid signatures found + */ + @Throws(IOException::class, SOPGPException::class) + fun toByteArrayAndResult() = + ByteArrayOutputStream().let { + val result = writeTo(it) + ByteArrayAndResult(it.toByteArray(), result) + } +} From 2391ffc9b2c5e6585f8eb24219e741eb7cfdd031 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:26:33 +0100 Subject: [PATCH 012/238] Kotlin conversion: DecryptionResult --- .../src/main/java/sop/DecryptionResult.java | 29 ------------------- .../src/main/kotlin/sop/DecryptionResult.kt | 16 ++++++++++ 2 files changed, 16 insertions(+), 29 deletions(-) delete mode 100644 sop-java/src/main/java/sop/DecryptionResult.java create mode 100644 sop-java/src/main/kotlin/sop/DecryptionResult.kt diff --git a/sop-java/src/main/java/sop/DecryptionResult.java b/sop-java/src/main/java/sop/DecryptionResult.java deleted file mode 100644 index 4f0e1ab..0000000 --- a/sop-java/src/main/java/sop/DecryptionResult.java +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import sop.util.Optional; - -public class DecryptionResult { - - private final Optional sessionKey; - private final List verifications; - - public DecryptionResult(SessionKey sessionKey, List verifications) { - this.sessionKey = Optional.ofNullable(sessionKey); - this.verifications = Collections.unmodifiableList(verifications); - } - - public Optional getSessionKey() { - return sessionKey; - } - - public List getVerifications() { - return new ArrayList<>(verifications); - } -} diff --git a/sop-java/src/main/kotlin/sop/DecryptionResult.kt b/sop-java/src/main/kotlin/sop/DecryptionResult.kt new file mode 100644 index 0000000..1ba98bb --- /dev/null +++ b/sop-java/src/main/kotlin/sop/DecryptionResult.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional + +data class DecryptionResult +internal constructor(val sessionKey: Optional, val verifications: List) { + + constructor( + sessionKey: SessionKey?, + verifications: List + ) : this(Optional.ofNullable(sessionKey), verifications) +} From dc23c8aa98a9f141af6051da1415b82dfe3a9f70 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:29:58 +0100 Subject: [PATCH 013/238] Kotlin conversion: Signatures --- sop-java/src/main/java/sop/Signatures.java | 21 --------------------- sop-java/src/main/kotlin/sop/Signatures.kt | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 21 deletions(-) delete mode 100644 sop-java/src/main/java/sop/Signatures.java create mode 100644 sop-java/src/main/kotlin/sop/Signatures.kt diff --git a/sop-java/src/main/java/sop/Signatures.java b/sop-java/src/main/java/sop/Signatures.java deleted file mode 100644 index dd3f000..0000000 --- a/sop-java/src/main/java/sop/Signatures.java +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import java.io.IOException; -import java.io.OutputStream; - -public abstract class Signatures extends Ready { - - /** - * Write OpenPGP signatures to the provided output stream. - * - * @param signatureOutputStream output stream - * @throws IOException in case of an IO error - */ - @Override - public abstract void writeTo(OutputStream signatureOutputStream) throws IOException; - -} diff --git a/sop-java/src/main/kotlin/sop/Signatures.kt b/sop-java/src/main/kotlin/sop/Signatures.kt new file mode 100644 index 0000000..63aafe9 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/Signatures.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import java.io.IOException +import java.io.OutputStream + +abstract class Signatures : Ready() { + + /** + * Write OpenPGP signatures to the provided output stream. + * + * @param outputStream signature output stream + * @throws IOException in case of an IO error + */ + @Throws(IOException::class) abstract override fun writeTo(outputStream: OutputStream) +} From 31409b7949415b995c5a3a7cf52ad4af40b640bd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:38:53 +0100 Subject: [PATCH 014/238] Kotlin conversion: SOP --- sop-java/src/main/java/sop/SOP.java | 177 ---------------------------- sop-java/src/main/kotlin/sop/SOP.kt | 97 +++++++++++++++ 2 files changed, 97 insertions(+), 177 deletions(-) delete mode 100644 sop-java/src/main/java/sop/SOP.java create mode 100644 sop-java/src/main/kotlin/sop/SOP.kt diff --git a/sop-java/src/main/java/sop/SOP.java b/sop-java/src/main/java/sop/SOP.java deleted file mode 100644 index 1200e21..0000000 --- a/sop-java/src/main/java/sop/SOP.java +++ /dev/null @@ -1,177 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop; - -import sop.operation.Armor; -import sop.operation.ChangeKeyPassword; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -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.DetachedSign; -import sop.operation.DetachedVerify; -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 operations. - *
- * Note: Subcommand objects acquired by calling any method of this interface are not intended for reuse. - * If you for example need to generate multiple keys, make a dedicated call to {@link #generateKey()} once per - * key generation. - */ -public interface SOP { - - /** - * Get information about the implementations name and version. - * - * @return version - */ - Version version(); - - /** - * Generate a secret key. - * Customize the operation using the builder {@link GenerateKey}. - * - * @return builder instance - */ - GenerateKey generateKey(); - - /** - * Extract a certificate (public key) from a secret key. - * Customize the operation using the builder {@link ExtractCert}. - * - * @return builder instance - */ - ExtractCert extractCert(); - - /** - * Create detached signatures. - * Customize the operation using the builder {@link DetachedSign}. - *

- * If you want to sign a message inline, use {@link #inlineSign()} instead. - * - * @return builder instance - */ - default DetachedSign sign() { - return detachedSign(); - } - - /** - * Create detached signatures. - * Customize the operation using the builder {@link DetachedSign}. - *

- * If you want to sign a message inline, use {@link #inlineSign()} instead. - * - * @return builder instance - */ - DetachedSign detachedSign(); - - /** - * Sign a message using inline signatures. - *

- * If you need to create detached signatures, use {@link #detachedSign()} instead. - * - * @return builder instance - */ - InlineSign inlineSign(); - - /** - * Verify detached signatures. - * Customize the operation using the builder {@link DetachedVerify}. - *

- * If you need to verify an inline-signed message, use {@link #inlineVerify()} instead. - * - * @return builder instance - */ - default DetachedVerify verify() { - return detachedVerify(); - } - - /** - * Verify detached signatures. - * Customize the operation using the builder {@link DetachedVerify}. - *

- * If you need to verify an inline-signed message, use {@link #inlineVerify()} instead. - * - * @return builder instance - */ - DetachedVerify detachedVerify(); - - /** - * Verify signatures of an inline-signed message. - *

- * If you need to verify detached signatures over a message, use {@link #detachedVerify()} instead. - * - * @return builder instance - */ - InlineVerify inlineVerify(); - - /** - * Detach signatures from an inline signed message. - * - * @return builder instance - */ - InlineDetach inlineDetach(); - - /** - * Encrypt a message. - * Customize the operation using the builder {@link Encrypt}. - * - * @return builder instance - */ - Encrypt encrypt(); - - /** - * Decrypt a message. - * Customize the operation using the builder {@link Decrypt}. - * - * @return builder instance - */ - Decrypt decrypt(); - - /** - * Convert binary OpenPGP data to ASCII. - * Customize the operation using the builder {@link Armor}. - * - * @return builder instance - */ - Armor armor(); - - /** - * Converts ASCII armored OpenPGP data to binary. - * Customize the operation using the builder {@link Dearmor}. - * - * @return builder instance - */ - Dearmor dearmor(); - - /** - * List supported {@link Profile Profiles} of a subcommand. - * - * @return builder instance - */ - ListProfiles listProfiles(); - - /** - * Revoke one or more secret keys. - * - * @return builder instance - */ - RevokeKey revokeKey(); - - /** - * Update a key's password. - * - * @return builder instance - */ - ChangeKeyPassword changeKeyPassword(); -} diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt new file mode 100644 index 0000000..e01763a --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.operation.Armor +import sop.operation.ChangeKeyPassword +import sop.operation.Dearmor +import sop.operation.Decrypt +import sop.operation.DetachedSign +import sop.operation.DetachedVerify +import sop.operation.Encrypt +import sop.operation.ExtractCert +import sop.operation.GenerateKey +import sop.operation.InlineDetach +import sop.operation.InlineSign +import sop.operation.InlineVerify +import sop.operation.ListProfiles +import sop.operation.RevokeKey +import sop.operation.Version + +/** + * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related + * operations. Note: Subcommand objects acquired by calling any method of this interface are not + * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to + * [generateKey] once per key generation. + */ +interface SOP { + + /** Get information about the implementations name and version. */ + fun version(): Version + + /** Generate a secret key. */ + fun generateKey(): GenerateKey + + /** Extract a certificate (public key) from a secret key. */ + fun extractCert(): ExtractCert + + /** + * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. + */ + fun sign(): DetachedSign = detachedSign() + + /** + * Create detached signatures. If you want to sign a message inline, use [inlineSign] instead. + */ + fun detachedSign(): DetachedSign + + /** + * Sign a message using inline signatures. If you need to create detached signatures, use + * [detachedSign] instead. + */ + 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. */ + fun inlineDetach(): InlineDetach + + /** Encrypt a message. */ + fun encrypt(): Encrypt + + /** Decrypt a message. */ + fun decrypt(): Decrypt + + /** Convert binary OpenPGP data to ASCII. */ + fun armor(): Armor + + /** Converts ASCII armored OpenPGP data to binary. */ + fun dearmor(): Dearmor + + /** List supported [Profiles][Profile] of a subcommand. */ + fun listProfiles(): ListProfiles + + /** Revoke one or more secret keys. */ + fun revokeKey(): RevokeKey + + /** Update a key's password. */ + fun changeKeyPassword(): ChangeKeyPassword +} From e68d6df57f86da9b4881205fe7f6b455d25c8690 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:50:46 +0100 Subject: [PATCH 015/238] Kotlin conversion: AbstractSign --- .../main/java/sop/operation/AbstractSign.java | 85 ------------------- .../main/kotlin/sop/operation/AbstractSign.kt | 79 +++++++++++++++++ 2 files changed, 79 insertions(+), 85 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/AbstractSign.java create mode 100644 sop-java/src/main/kotlin/sop/operation/AbstractSign.kt diff --git a/sop-java/src/main/java/sop/operation/AbstractSign.java b/sop-java/src/main/java/sop/operation/AbstractSign.java deleted file mode 100644 index 508d741..0000000 --- a/sop-java/src/main/java/sop/operation/AbstractSign.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface AbstractSign { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - T noArmor(); - - /** - * Add one or more signing keys. - * - * @param key input stream containing encoded keys - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - T key(InputStream key) - throws SOPGPException.KeyCannotSign, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; - - /** - * Add one or more signing keys. - * - * @param key byte array containing encoded keys - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - default T key(byte[] key) - throws SOPGPException.KeyCannotSign, - SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException { - return key(new ByteArrayInputStream(key)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the provided passphrase is not human-readable - */ - default T withKeyPassword(String password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the provided passphrase is not human-readable - */ - T withKeyPassword(byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable; - -} diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt new file mode 100644 index 0000000..0258432 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyCannotSign +import sop.exception.SOPGPException.PasswordNotHumanReadable +import sop.exception.SOPGPException.UnsupportedAsymmetricAlgo +import sop.exception.SOPGPException.UnsupportedOption +import sop.util.UTF8Util + +/** + * Interface for signing operations. + * + * @param builder subclass + */ +interface AbstractSign { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): T + + /** + * Add one or more signing keys. + * + * @param key input stream containing encoded keys + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws BadData if the [InputStream] does not contain an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun key(key: InputStream): T + + /** + * Add one or more signing keys. + * + * @param key byte array containing encoded keys + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws BadData if the byte array does not contain an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun key(key: ByteArray): T = key(key.inputStream()) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: String): T = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: ByteArray): T +} From 08ddc5d8a529aa1acfb65b7e849a82b2ca596534 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:51:30 +0100 Subject: [PATCH 016/238] Kotlin conversion: AbstractVerify --- .../java/sop/operation/AbstractVerify.java | 68 ------------------- .../kotlin/sop/operation/AbstractVerify.kt | 57 ++++++++++++++++ 2 files changed, 57 insertions(+), 68 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/AbstractVerify.java create mode 100644 sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt diff --git a/sop-java/src/main/java/sop/operation/AbstractVerify.java b/sop-java/src/main/java/sop/operation/AbstractVerify.java deleted file mode 100644 index 51d84b7..0000000 --- a/sop-java/src/main/java/sop/operation/AbstractVerify.java +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -/** - * Common API methods shared between verification of inline signatures ({@link InlineVerify}) - * and verification of detached signatures ({@link DetachedVerify}). - * - * @param Builder type ({@link DetachedVerify}, {@link InlineVerify}) - */ -public interface AbstractVerify { - - /** - * Makes the SOP implementation consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - T notBefore(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP implementation consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - */ - T notAfter(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Add one or more verification cert. - * - * @param cert input stream containing the encoded certs - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - T cert(InputStream cert) - throws SOPGPException.BadData, - IOException; - - /** - * Add one or more verification cert. - * - * @param cert byte array containing the encoded certs - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - default T cert(byte[] cert) - throws SOPGPException.BadData, - IOException { - return cert(new ByteArrayInputStream(cert)); - } - -} diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt b/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt new file mode 100644 index 0000000..3554ea3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/AbstractVerify.kt @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import java.util.* +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +/** + * Common API methods shared between verification of inline signatures ([InlineVerify]) and + * verification of detached signatures ([DetachedVerify]). + * + * @param Builder type ([DetachedVerify], [InlineVerify]) + */ +interface AbstractVerify { + + /** + * Makes the SOP implementation consider signatures before this date invalid. + * + * @param timestamp timestamp + * @return builder instance + */ + @Throws(UnsupportedOption::class) fun notBefore(timestamp: Date): T + + /** + * Makes the SOP implementation consider signatures after this date invalid. + * + * @param timestamp timestamp + * @return builder instance + */ + @Throws(UnsupportedOption::class) fun notAfter(timestamp: Date): T + + /** + * Add one or more verification cert. + * + * @param cert input stream containing the encoded certs + * @return builder instance + * @throws BadData if the input stream does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) fun cert(cert: InputStream): T + + /** + * Add one or more verification cert. + * + * @param cert byte array containing the encoded certs + * @return builder instance + * @throws BadData if the byte array does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun cert(cert: ByteArray): T = cert(cert.inputStream()) +} From 4a123a198078f81c80640716d51c574a956dccda Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 13:54:24 +0100 Subject: [PATCH 017/238] Kotlin conversion: Armor --- .../src/main/java/sop/operation/Armor.java | 53 ------------------- .../src/main/kotlin/sop/operation/Armor.kt | 46 ++++++++++++++++ 2 files changed, 46 insertions(+), 53 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Armor.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Armor.kt diff --git a/sop-java/src/main/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java deleted file mode 100644 index a625808..0000000 --- a/sop-java/src/main/java/sop/operation/Armor.java +++ /dev/null @@ -1,53 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; - -public interface Armor { - - /** - * Overrides automatic detection of label. - * - * @param label armor label - * @return builder instance - */ - Armor label(ArmorLabel label) - throws SOPGPException.UnsupportedOption; - - /** - * Armor the provided data. - * - * @param data input stream of unarmored OpenPGP data - * @return armored data - * - * @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken - * @throws IOException in case of an IO error - */ - Ready data(InputStream data) - throws SOPGPException.BadData, - IOException; - - /** - * Armor the provided data. - * - * @param data unarmored OpenPGP data - * @return armored data - * - * @throws sop.exception.SOPGPException.BadData if the data appears to be OpenPGP packets, but those are broken - * @throws IOException in case of an IO error - */ - default Ready data(byte[] data) - throws SOPGPException.BadData, - IOException { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt new file mode 100644 index 0000000..e89708b --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.enums.ArmorLabel +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +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. + * + * @param data input stream of unarmored OpenPGP data + * @return armored data + * @throws BadData if the data appears to be OpenPGP packets, but those are broken + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready + + /** + * Armor the provided data. + * + * @param data unarmored OpenPGP data + * @return armored data + * @throws BadData if the data appears to be OpenPGP packets, but those are broken + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun data(data: ByteArray): Ready = data(data.inputStream()) +} From 34e1d8992f2b6adb626dd25dd6c940a233a687bc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:00:49 +0100 Subject: [PATCH 018/238] Kotlin conversion: ChangeKeyPassword --- .../java/sop/operation/ChangeKeyPassword.java | 83 ---------------- .../kotlin/sop/operation/ChangeKeyPassword.kt | 95 +++++++++++++++++++ 2 files changed, 95 insertions(+), 83 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/ChangeKeyPassword.java create mode 100644 sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt diff --git a/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java b/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java deleted file mode 100644 index 460a20a..0000000 --- a/sop-java/src/main/java/sop/operation/ChangeKeyPassword.java +++ /dev/null @@ -1,83 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.nio.charset.CharacterCodingException; - -public interface ChangeKeyPassword { - - /** - * Disable ASCII armoring of the output. - * - * @return builder instance - */ - ChangeKeyPassword noArmor(); - - default ChangeKeyPassword oldKeyPassphrase(byte[] password) { - try { - return oldKeyPassphrase(UTF8Util.decodeUTF8(password)); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string."); - } - } - - /** - * Provide a passphrase to unlock the secret key. - * This method can be provided multiple times to provide separate passphrases that are tried as a - * means to unlock any secret key material encountered. - * - * @param oldPassphrase old passphrase - * @return builder instance - */ - ChangeKeyPassword oldKeyPassphrase(String oldPassphrase); - - /** - * Provide a passphrase to re-lock the secret key with. - * This method can only be used once, and all key material encountered will be encrypted with the given passphrase. - * If this method is not called, the key material will not be protected. - * - * @param newPassphrase new passphrase - * @return builder instance - */ - default ChangeKeyPassword newKeyPassphrase(byte[] newPassphrase) { - try { - return newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable("Password MUST be a valid UTF8 string."); - } - } - - /** - * Provide a passphrase to re-lock the secret key with. - * This method can only be used once, and all key material encountered will be encrypted with the given passphrase. - * If this method is not called, the key material will not be protected. - * - * @param newPassphrase new passphrase - * @return builder instance - */ - ChangeKeyPassword newKeyPassphrase(String newPassphrase); - - default Ready keys(byte[] keys) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { - return keys(new ByteArrayInputStream(keys)); - } - - /** - * Provide the key material. - * - * @param inputStream input stream of secret key material - * @return ready - * - * @throws sop.exception.SOPGPException.KeyIsProtected if any (sub-) key encountered cannot be unlocked. - * @throws sop.exception.SOPGPException.BadData if the key material is malformed - */ - Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData; - -} diff --git a/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt new file mode 100644 index 0000000..224e0f4 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt @@ -0,0 +1,95 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyIsProtected +import sop.exception.SOPGPException.PasswordNotHumanReadable +import sop.util.UTF8Util + +interface ChangeKeyPassword { + + /** + * Disable ASCII armoring of the output. + * + * @return builder instance + */ + fun noArmor(): ChangeKeyPassword + + /** + * Provide a passphrase to unlock the secret key. This method can be provided multiple times to + * provide separate passphrases that are tried as a means to unlock any secret key material + * encountered. + * + * @param oldPassphrase old passphrase + * @return builder instance + */ + @Throws(PasswordNotHumanReadable::class) + fun oldKeyPassphrase(oldPassphrase: ByteArray): ChangeKeyPassword = + try { + oldKeyPassphrase(UTF8Util.decodeUTF8(oldPassphrase)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") + } + + /** + * Provide a passphrase to unlock the secret key. This method can be provided multiple times to + * provide separate passphrases that are tried as a means to unlock any secret key material + * encountered. + * + * @param oldPassphrase old passphrase + * @return builder instance + */ + fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword + + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + @Throws(PasswordNotHumanReadable::class) + fun newKeyPassphrase(newPassphrase: ByteArray): ChangeKeyPassword = + try { + newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") + } + + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword + + /** + * Provide the key material. + * + * @param keys input stream of secret key material + * @return ready + * @throws KeyIsProtected if any (sub-) key encountered cannot be unlocked. + * @throws BadData if the key material is malformed + */ + @Throws(KeyIsProtected::class, BadData::class) + fun keys(keys: ByteArray): Ready = keys(keys.inputStream()) + + /** + * Provide the key material. + * + * @param keys input stream of secret key material + * @return ready + * @throws KeyIsProtected if any (sub-) key encountered cannot be unlocked. + * @throws BadData if the key material is malformed + */ + @Throws(KeyIsProtected::class, BadData::class) fun keys(keys: InputStream): Ready +} From 39c222dfc893d780d8daaee8fd47854ecb17d143 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:03:18 +0100 Subject: [PATCH 019/238] Kotlin conversion: Dearmor --- .../src/main/java/sop/operation/Dearmor.java | 59 ------------------- .../src/main/kotlin/sop/operation/Dearmor.kt | 46 +++++++++++++++ 2 files changed, 46 insertions(+), 59 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Dearmor.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Dearmor.kt diff --git a/sop-java/src/main/java/sop/operation/Dearmor.java b/sop-java/src/main/java/sop/operation/Dearmor.java deleted file mode 100644 index 524dc8c..0000000 --- a/sop-java/src/main/java/sop/operation/Dearmor.java +++ /dev/null @@ -1,59 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -public interface Dearmor { - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - * - * @throws SOPGPException.BadData in case of non-OpenPGP data - * @throws IOException in case of an IO error - */ - Ready data(InputStream data) - throws SOPGPException.BadData, - IOException; - - /** - * Dearmor armored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - * - * @throws SOPGPException.BadData in case of non-OpenPGP data - * @throws IOException in case of an IO error - */ - default Ready data(byte[] data) - throws SOPGPException.BadData, - IOException { - return data(new ByteArrayInputStream(data)); - } - - /** - * Dearmor amored OpenPGP data. - * - * @param data armored OpenPGP data - * @return input stream of unarmored data - * - * @throws SOPGPException.BadData in case of non-OpenPGP data - * @throws IOException in case of an IO error - */ - default Ready data(String data) - throws SOPGPException.BadData, - IOException { - return data(data.getBytes(UTF8Util.UTF8)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt new file mode 100644 index 0000000..cc5e98d --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.BadData +import sop.util.UTF8Util + +interface Dearmor { + + /** + * Dearmor armored OpenPGP data. + * + * @param data armored OpenPGP data + * @return input stream of unarmored data + * @throws BadData in case of non-OpenPGP data + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) fun data(data: InputStream): Ready + + /** + * Dearmor armored OpenPGP data. + * + * @param data armored OpenPGP data + * @return input stream of unarmored data + * @throws BadData in case of non-OpenPGP data + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun data(data: ByteArray): Ready = data(data.inputStream()) + + /** + * Dearmor amored OpenPGP data. + * + * @param data armored OpenPGP data + * @return input stream of unarmored data + * @throws BadData in case of non-OpenPGP data + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun data(data: String): Ready = data(data.toByteArray(UTF8Util.UTF8)) +} From 91a861b5c366d54ddff4b1625e920e0e75d476cb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:08:03 +0100 Subject: [PATCH 020/238] Kotlin conversion: Decrypt --- .../src/main/java/sop/operation/Decrypt.java | 193 ------------------ .../src/main/kotlin/sop/operation/Decrypt.kt | 169 +++++++++++++++ 2 files changed, 169 insertions(+), 193 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Decrypt.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Decrypt.kt diff --git a/sop-java/src/main/java/sop/operation/Decrypt.java b/sop-java/src/main/java/sop/operation/Decrypt.java deleted file mode 100644 index 0123bbc..0000000 --- a/sop-java/src/main/java/sop/operation/Decrypt.java +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.Date; - -public interface Decrypt { - - /** - * Makes the SOP consider signatures before this date invalid. - * - * @param timestamp timestamp - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt verifyNotBefore(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Makes the SOP consider signatures after this date invalid. - * - * @param timestamp timestamp - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt verifyNotAfter(Date timestamp) - throws SOPGPException.UnsupportedOption; - - /** - * Adds one or more verification cert. - * - * @param cert input stream containing the cert(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} doesn't provide an OpenPGP certificate - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - Decrypt verifyWithCert(InputStream cert) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; - - /** - * Adds one or more verification cert. - * - * @param cert byte array containing the cert(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array doesn't contain an OpenPGP certificate - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - default Decrypt verifyWithCert(byte[] cert) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException { - return verifyWithCert(new ByteArrayInputStream(cert)); - } - - /** - * Tries to decrypt with the given session key. - * - * @param sessionKey session key - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt withSessionKey(SessionKey sessionKey) - throws SOPGPException.UnsupportedOption; - - /** - * Tries to decrypt with the given password. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Decrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Adds one or more decryption key. - * - * @param key input stream containing the key(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not provide an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - Decrypt withKey(InputStream key) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; - - /** - * Adds one or more decryption key. - * - * @param key byte array containing the key(s) - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - default Decrypt withKey(byte[] key) - throws SOPGPException.BadData, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException { - return withKey(new ByteArrayInputStream(key)); - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - default Decrypt withKeyPassword(String password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws sop.exception.SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - Decrypt withKeyPassword(byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable; - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - * - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not provide an OpenPGP message - * @throws sop.exception.SOPGPException.MissingArg if an argument required for decryption was not provided - * @throws sop.exception.SOPGPException.CannotDecrypt in case decryption fails for some reason - * @throws sop.exception.SOPGPException.KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) - * @throws IOException in case of an IO error - */ - ReadyWithResult ciphertext(InputStream ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg, - SOPGPException.CannotDecrypt, - SOPGPException.KeyIsProtected, - IOException; - - /** - * Decrypts the given ciphertext, returning verification results and plaintext. - * @param ciphertext ciphertext - * @return ready with result - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an encrypted OpenPGP message - * @throws sop.exception.SOPGPException.MissingArg in case of missing decryption method (password or key required) - * @throws sop.exception.SOPGPException.CannotDecrypt in case decryption fails for some reason - * @throws sop.exception.SOPGPException.KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) - * @throws IOException in case of an IO error - */ - default ReadyWithResult ciphertext(byte[] ciphertext) - throws SOPGPException.BadData, - SOPGPException.MissingArg, - SOPGPException.CannotDecrypt, - SOPGPException.KeyIsProtected, - IOException { - return ciphertext(new ByteArrayInputStream(ciphertext)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt new file mode 100644 index 0000000..9c24c54 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import java.util.* +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.exception.SOPGPException.* +import sop.util.UTF8Util + +interface Decrypt { + + /** + * Makes the SOP consider signatures before this date invalid. + * + * @param timestamp timestamp + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun verifyNotBefore(timestamp: Date): Decrypt + + /** + * Makes the SOP consider signatures after this date invalid. + * + * @param timestamp timestamp + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun verifyNotAfter(timestamp: Date): Decrypt + + /** + * Adds one or more verification cert. + * + * @param cert input stream containing the cert(s) + * @return builder instance + * @throws BadData if the [InputStream] doesn't provide an OpenPGP certificate + * @throws UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun verifyWithCert(cert: InputStream): Decrypt + + /** + * Adds one or more verification cert. + * + * @param cert byte array containing the cert(s) + * @return builder instance + * @throws BadData if the byte array doesn't contain an OpenPGP certificate + * @throws UnsupportedAsymmetricAlgo if the cert uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun verifyWithCert(cert: ByteArray): Decrypt = verifyWithCert(cert.inputStream()) + + /** + * Tries to decrypt with the given session key. + * + * @param sessionKey session key + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun withSessionKey(sessionKey: SessionKey): Decrypt + + /** + * Tries to decrypt with the given password. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if this option is not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withPassword(password: String): Decrypt + + /** + * Adds one or more decryption key. + * + * @param key input stream containing the key(s) + * @return builder instance + * @throws BadData if the [InputStream] does not provide an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun withKey(key: InputStream): Decrypt + + /** + * Adds one or more decryption key. + * + * @param key byte array containing the key(s) + * @return builder instance + * @throws BadData if the byte array does not contain an OpenPGP key + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun withKey(key: ByteArray): Decrypt { + return withKey(key.inputStream()) + } + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: String): Decrypt { + return withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + } + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: ByteArray): Decrypt + + /** + * Decrypts the given ciphertext, returning verification results and plaintext. + * + * @param ciphertext ciphertext + * @return ready with result + * @throws BadData if the [InputStream] does not provide an OpenPGP message + * @throws MissingArg if an argument required for decryption was not provided + * @throws CannotDecrypt in case decryption fails for some reason + * @throws KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) + * @throws IOException in case of an IO error + */ + @Throws( + BadData::class, + MissingArg::class, + CannotDecrypt::class, + KeyIsProtected::class, + IOException::class) + fun ciphertext(ciphertext: InputStream): ReadyWithResult + + /** + * Decrypts the given ciphertext, returning verification results and plaintext. + * + * @param ciphertext ciphertext + * @return ready with result + * @throws BadData if the byte array does not contain an encrypted OpenPGP message + * @throws MissingArg in case of missing decryption method (password or key required) + * @throws CannotDecrypt in case decryption fails for some reason + * @throws KeyIsProtected if the decryption key cannot be unlocked (e.g. missing passphrase) + * @throws IOException in case of an IO error + */ + @Throws( + BadData::class, + MissingArg::class, + CannotDecrypt::class, + KeyIsProtected::class, + IOException::class) + fun ciphertext(ciphertext: ByteArray): ReadyWithResult { + return ciphertext(ciphertext.inputStream()) + } +} From 4dc1779a06be4aadffdd39855cb6e4da081cde00 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:10:06 +0100 Subject: [PATCH 021/238] Kotlin conversion: DetachedSign --- .../main/java/sop/operation/DetachedSign.java | 61 ------------------- .../main/kotlin/sop/operation/DetachedSign.kt | 50 +++++++++++++++ 2 files changed, 50 insertions(+), 61 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/DetachedSign.java create mode 100644 sop-java/src/main/kotlin/sop/operation/DetachedSign.kt diff --git a/sop-java/src/main/java/sop/operation/DetachedSign.java b/sop-java/src/main/java/sop/operation/DetachedSign.java deleted file mode 100644 index 745077d..0000000 --- a/sop-java/src/main/java/sop/operation/DetachedSign.java +++ /dev/null @@ -1,61 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface DetachedSign extends AbstractSign { - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - DetachedSign mode(SignAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - ReadyWithResult data(InputStream data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - default ReadyWithResult data(byte[] data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt new file mode 100644 index 0000000..c0e62dd --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.ReadyWithResult +import sop.SigningResult +import sop.enums.SignAs +import sop.exception.SOPGPException.* + +interface DetachedSign : AbstractSign { + + /** + * Sets the signature mode. Note: This method has to be called before [key] is called. + * + * @param mode signature mode + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun mode(mode: SignAs): DetachedSign + + /** + * Signs data. + * + * @param data input stream containing data + * @return ready + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + * @throws ExpectedText if text data was expected, but binary data was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: InputStream): ReadyWithResult + + /** + * Signs data. + * + * @param data byte array containing data + * @return ready + * @throws IOException in case of an IO error + * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be + * unlocked + * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data + * was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: ByteArray): ReadyWithResult = data(data.inputStream()) +} From ee6975c7d3ab039b917aef476de44af7f542bef2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:11:11 +0100 Subject: [PATCH 022/238] Decrypt: Use return statement --- sop-java/src/main/kotlin/sop/operation/Decrypt.kt | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt index 9c24c54..ae228e9 100644 --- a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt @@ -99,9 +99,7 @@ interface Decrypt { * @throws IOException in case of an IO error */ @Throws(BadData::class, UnsupportedAsymmetricAlgo::class, IOException::class) - fun withKey(key: ByteArray): Decrypt { - return withKey(key.inputStream()) - } + fun withKey(key: ByteArray): Decrypt = withKey(key.inputStream()) /** * Provide the decryption password for the secret key. @@ -112,9 +110,8 @@ interface Decrypt { * @throws PasswordNotHumanReadable if the password is not human-readable */ @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) - fun withKeyPassword(password: String): Decrypt { - return withKeyPassword(password.toByteArray(UTF8Util.UTF8)) - } + fun withKeyPassword(password: String): Decrypt = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) /** * Provide the decryption password for the secret key. @@ -163,7 +160,6 @@ interface Decrypt { CannotDecrypt::class, KeyIsProtected::class, IOException::class) - fun ciphertext(ciphertext: ByteArray): ReadyWithResult { - return ciphertext(ciphertext.inputStream()) - } + fun ciphertext(ciphertext: ByteArray): ReadyWithResult = + ciphertext(ciphertext.inputStream()) } From e681090757312dedcc90cc9fc35ee7f64118c99d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:12:56 +0100 Subject: [PATCH 023/238] Kotlin conversion DetachedVerify --- .../java/sop/operation/DetachedVerify.java | 45 ------------------- .../kotlin/sop/operation/DetachedVerify.kt | 36 +++++++++++++++ 2 files changed, 36 insertions(+), 45 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/DetachedVerify.java create mode 100644 sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt diff --git a/sop-java/src/main/java/sop/operation/DetachedVerify.java b/sop-java/src/main/java/sop/operation/DetachedVerify.java deleted file mode 100644 index 9dee870..0000000 --- a/sop-java/src/main/java/sop/operation/DetachedVerify.java +++ /dev/null @@ -1,45 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -/** - * API for verifying detached signatures. - */ -public interface DetachedVerify extends AbstractVerify, VerifySignatures { - - /** - * Provides the detached signatures. - * @param signatures input stream containing encoded, detached signatures. - * - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain OpenPGP signatures - * @throws IOException in case of an IO error - */ - VerifySignatures signatures(InputStream signatures) - throws SOPGPException.BadData, - IOException; - - /** - * Provides the detached signatures. - * @param signatures byte array containing encoded, detached signatures. - * - * @return builder instance - * - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain OpenPGP signatures - * @throws IOException in case of an IO error - */ - default VerifySignatures signatures(byte[] signatures) - throws SOPGPException.BadData, - IOException { - return signatures(new ByteArrayInputStream(signatures)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt new file mode 100644 index 0000000..2dd9218 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import sop.exception.SOPGPException.BadData + +interface DetachedVerify : AbstractVerify, VerifySignatures { + + /** + * Provides the detached signatures. + * + * @param signatures input stream containing encoded, detached signatures. + * @return builder instance + * @throws BadData if the input stream does not contain OpenPGP signatures + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun signatures(signatures: InputStream): VerifySignatures + + /** + * Provides the detached signatures. + * + * @param signatures byte array containing encoded, detached signatures. + * @return builder instance + * @throws BadData if the byte array does not contain OpenPGP signatures + * @throws IOException in case of an IO error + */ + @Throws(BadData::class, IOException::class) + fun signatures(signatures: ByteArray): VerifySignatures = + signatures(ByteArrayInputStream(signatures)) +} From 41db9d2ac77d7b8e5c2fe79ae02ec36efb6bc240 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:15:56 +0100 Subject: [PATCH 024/238] Kotlin conversion: Encrypt --- .../src/main/java/sop/operation/Encrypt.java | 193 ------------------ .../src/main/kotlin/sop/operation/Encrypt.kt | 166 +++++++++++++++ 2 files changed, 166 insertions(+), 193 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Encrypt.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Encrypt.kt diff --git a/sop-java/src/main/java/sop/operation/Encrypt.java b/sop-java/src/main/java/sop/operation/Encrypt.java deleted file mode 100644 index b380d32..0000000 --- a/sop-java/src/main/java/sop/operation/Encrypt.java +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Profile; -import sop.Ready; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface Encrypt { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - Encrypt noArmor(); - - /** - * Sets encryption mode. - * - * @param mode mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Encrypt mode(EncryptAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Adds the signer key. - * - * @param key input stream containing the encoded signer key - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key - * @throws IOException in case of an IO error - */ - Encrypt signWith(InputStream key) - throws SOPGPException.KeyCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException; - - /** - * Adds the signer key. - * - * @param key byte array containing the encoded signer key - * @return builder instance - * - * @throws sop.exception.SOPGPException.KeyCannotSign if the key cannot be used for signing - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - * @throws IOException in case of an IO error - */ - default Encrypt signWith(byte[] key) - throws SOPGPException.KeyCannotSign, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException { - return signWith(new ByteArrayInputStream(key)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if key password are not supported - */ - default Encrypt withKeyPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the password for the secret key used for signing. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if key password are not supported - */ - Encrypt withKeyPassword(byte[] password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Encrypt with the given password. - * - * @param password password - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - Encrypt withPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Encrypt with the given cert. - * - * @param cert input stream containing the encoded cert. - * @return builder instance - * - * @throws sop.exception.SOPGPException.CertCannotEncrypt if the certificate is not encryption capable - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - Encrypt withCert(InputStream cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException; - - /** - * Encrypt with the given cert. - * - * @param cert byte array containing the encoded cert. - * @return builder instance - * - * @throws sop.exception.SOPGPException.CertCannotEncrypt if the certificate is not encryption capable - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP certificate - * @throws IOException in case of an IO error - */ - default Encrypt withCert(byte[] cert) - throws SOPGPException.CertCannotEncrypt, - SOPGPException.UnsupportedAsymmetricAlgo, - SOPGPException.BadData, - IOException { - return withCert(new ByteArrayInputStream(cert)); - } - - /** - * Pass in a profile. - * - * @param profile profile - * @return builder instance - */ - default Encrypt profile(Profile profile) { - return profile(profile.getName()); - } - - /** - * Pass in a profile identifier. - * - * @param profileName profile identifier - * @return builder instance - */ - Encrypt profile(String profileName); - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - */ - Ready plaintext(InputStream plaintext) - throws IOException, - SOPGPException.KeyIsProtected; - - /** - * Encrypt the given data yielding the ciphertext. - * @param plaintext plaintext - * @return input stream containing the ciphertext - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - */ - default Ready plaintext(byte[] plaintext) - throws IOException, - SOPGPException.KeyIsProtected { - return plaintext(new ByteArrayInputStream(plaintext)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt new file mode 100644 index 0000000..ad30149 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -0,0 +1,166 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import sop.Profile +import sop.Ready +import sop.enums.EncryptAs +import sop.exception.SOPGPException.* +import sop.util.UTF8Util + +interface Encrypt { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): Encrypt + + /** + * Sets encryption mode. + * + * @param mode mode + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun mode(mode: EncryptAs): Encrypt + + /** + * Adds the signer key. + * + * @param key input stream containing the encoded signer key + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws BadData if the [InputStream] does not contain an OpenPGP key + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) + fun signWith(key: InputStream): Encrypt + + /** + * Adds the signer key. + * + * @param key byte array containing the encoded signer key + * @return builder instance + * @throws KeyCannotSign if the key cannot be used for signing + * @throws UnsupportedAsymmetricAlgo if the key uses an unsupported asymmetric algorithm + * @throws BadData if the byte array does not contain an OpenPGP key + * @throws IOException in case of an IO error + */ + @Throws( + KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) + fun signWith(key: ByteArray): Encrypt = signWith(ByteArrayInputStream(key)) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if key password are not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: String): Encrypt = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if key password are not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): Encrypt + + /** + * Encrypt with the given password. + * + * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if this option is not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withPassword(password: String): Encrypt + + /** + * Encrypt with the given cert. + * + * @param cert input stream containing the encoded cert. + * @return builder instance + * @throws CertCannotEncrypt if the certificate is not encryption capable + * @throws UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm + * @throws BadData if the [InputStream] does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws( + CertCannotEncrypt::class, + UnsupportedAsymmetricAlgo::class, + BadData::class, + IOException::class) + fun withCert(cert: InputStream): Encrypt + + /** + * Encrypt with the given cert. + * + * @param cert byte array containing the encoded cert. + * @return builder instance + * @throws CertCannotEncrypt if the certificate is not encryption capable + * @throws UnsupportedAsymmetricAlgo if the certificate uses an unsupported asymmetric algorithm + * @throws BadData if the byte array does not contain an OpenPGP certificate + * @throws IOException in case of an IO error + */ + @Throws( + CertCannotEncrypt::class, + UnsupportedAsymmetricAlgo::class, + BadData::class, + IOException::class) + fun withCert(cert: ByteArray): Encrypt = withCert(ByteArrayInputStream(cert)) + + /** + * Pass in a profile. + * + * @param profile profile + * @return builder instance + */ + fun profile(profile: Profile): Encrypt = profile(profile.name) + + /** + * Pass in a profile identifier. + * + * @param profileName profile identifier + * @return builder instance + */ + fun profile(profileName: String): Encrypt + + /** + * Encrypt the given data yielding the ciphertext. + * + * @param plaintext plaintext + * @return input stream containing the ciphertext + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + */ + @Throws(IOException::class, KeyIsProtected::class) fun plaintext(plaintext: InputStream): Ready + + /** + * Encrypt the given data yielding the ciphertext. + * + * @param plaintext plaintext + * @return input stream containing the ciphertext + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + */ + @Throws(IOException::class, KeyIsProtected::class) + fun plaintext(plaintext: ByteArray): Ready = plaintext(ByteArrayInputStream(plaintext)) +} From 653675f730788593566aba75879870d4e39f652e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:17:12 +0100 Subject: [PATCH 025/238] Kotlin conversion: ExtractCert --- .../main/java/sop/operation/ExtractCert.java | 50 ------------------- .../main/kotlin/sop/operation/ExtractCert.kt | 42 ++++++++++++++++ 2 files changed, 42 insertions(+), 50 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/ExtractCert.java create mode 100644 sop-java/src/main/kotlin/sop/operation/ExtractCert.kt diff --git a/sop-java/src/main/java/sop/operation/ExtractCert.java b/sop-java/src/main/java/sop/operation/ExtractCert.java deleted file mode 100644 index a862d33..0000000 --- a/sop-java/src/main/java/sop/operation/ExtractCert.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.Ready; -import sop.exception.SOPGPException; - -public interface ExtractCert { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - ExtractCert noArmor(); - - /** - * Extract the cert(s) from the provided key(s). - * - * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys - * @return result containing the encoding of the keys certs - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the {@link InputStream} does not contain an OpenPGP key - */ - Ready key(InputStream keyInputStream) - throws IOException, - SOPGPException.BadData; - - /** - * Extract the cert(s) from the provided key(s). - * - * @param key byte array containing the encoding of one or more OpenPGP key - * @return result containing the encoding of the keys certs - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain an OpenPGP key - */ - default Ready key(byte[] key) - throws IOException, - SOPGPException.BadData { - return key(new ByteArrayInputStream(key)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt new file mode 100644 index 0000000..bd48e08 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.ByteArrayInputStream +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.BadData + +interface ExtractCert { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): ExtractCert + + /** + * Extract the cert(s) from the provided key(s). + * + * @param keyInputStream input stream containing the encoding of one or more OpenPGP keys + * @return result containing the encoding of the keys certs + * @throws IOException in case of an IO error + * @throws BadData if the [InputStream] does not contain an OpenPGP key + */ + @Throws(IOException::class, BadData::class) fun key(keyInputStream: InputStream): Ready + + /** + * Extract the cert(s) from the provided key(s). + * + * @param key byte array containing the encoding of one or more OpenPGP key + * @return result containing the encoding of the keys certs + * @throws IOException in case of an IO error + * @throws BadData if the byte array does not contain an OpenPGP key + */ + @Throws(IOException::class, BadData::class) + fun key(key: ByteArray): Ready = key(ByteArrayInputStream(key)) +} From 3e6ebe1cc44968b1ecad4d6244fc43f874a2aa64 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:19:08 +0100 Subject: [PATCH 026/238] Kotlin conversion: GenerateKey --- .../main/java/sop/operation/GenerateKey.java | 103 ------------------ .../main/kotlin/sop/operation/GenerateKey.kt | 91 ++++++++++++++++ 2 files changed, 91 insertions(+), 103 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/GenerateKey.java create mode 100644 sop-java/src/main/kotlin/sop/operation/GenerateKey.kt diff --git a/sop-java/src/main/java/sop/operation/GenerateKey.java b/sop-java/src/main/java/sop/operation/GenerateKey.java deleted file mode 100644 index 77afea1..0000000 --- a/sop-java/src/main/java/sop/operation/GenerateKey.java +++ /dev/null @@ -1,103 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.CharacterCodingException; - -import sop.Profile; -import sop.Ready; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -public interface GenerateKey { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - GenerateKey noArmor(); - - /** - * Adds a user-id. - * - * @param userId user-id - * @return builder instance - */ - GenerateKey userId(String userId); - - /** - * Set a password for the key. - * - * @param password password to protect the key - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - GenerateKey withKeyPassword(String password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption; - - /** - * Set a password for the key. - * - * @param password password to protect the key - * @return builder instance - * - * @throws sop.exception.SOPGPException.PasswordNotHumanReadable if the password is not human-readable - * @throws sop.exception.SOPGPException.UnsupportedOption if key passwords are not supported - */ - default GenerateKey withKeyPassword(byte[] password) - throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption { - try { - return withKeyPassword(UTF8Util.decodeUTF8(password)); - } catch (CharacterCodingException e) { - throw new SOPGPException.PasswordNotHumanReadable(); - } - } - - /** - * Pass in a profile. - * - * @param profile profile - * @return builder instance - */ - default GenerateKey profile(Profile profile) { - return profile(profile.getName()); - } - - /** - * Pass in a profile identifier. - * - * @param profile profile identifier - * @return builder instance - */ - GenerateKey profile(String profile); - - /** - * If this options is set, the generated key will not be capable of encryption / decryption. - * - * @return builder instance - */ - GenerateKey signingOnly(); - - /** - * Generate the OpenPGP key and return it encoded as an {@link InputStream}. - * - * @return key - * - * @throws sop.exception.SOPGPException.MissingArg if no user-id was provided - * @throws sop.exception.SOPGPException.UnsupportedAsymmetricAlgo if the generated key uses an unsupported asymmetric algorithm - * @throws IOException in case of an IO error - */ - Ready generate() - throws SOPGPException.MissingArg, - SOPGPException.UnsupportedAsymmetricAlgo, - IOException; -} diff --git a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt new file mode 100644 index 0000000..3b83b99 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import sop.Profile +import sop.Ready +import sop.exception.SOPGPException.* +import sop.util.UTF8Util + +interface GenerateKey { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): GenerateKey + + /** + * Adds a user-id. + * + * @param userId user-id + * @return builder instance + */ + fun userId(userId: String): GenerateKey + + /** + * Set a password for the key. + * + * @param password password to protect the key + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: String): GenerateKey + + /** + * Set a password for the key. + * + * @param password password to protect the key + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) + fun withKeyPassword(password: ByteArray): GenerateKey = + try { + withKeyPassword(UTF8Util.decodeUTF8(password)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable() + } + + /** + * Pass in a profile. + * + * @param profile profile + * @return builder instance + */ + fun profile(profile: Profile): GenerateKey = profile(profile.name) + + /** + * Pass in a profile identifier. + * + * @param profile profile identifier + * @return builder instance + */ + fun profile(profile: String): GenerateKey + + /** + * If this options is set, the generated key will not be capable of encryption / decryption. + * + * @return builder instance + */ + fun signingOnly(): GenerateKey + + /** + * Generate the OpenPGP key and return it encoded as an [InputStream]. + * + * @return key + * @throws MissingArg if no user-id was provided + * @throws UnsupportedAsymmetricAlgo if the generated key uses an unsupported asymmetric + * algorithm + * @throws IOException in case of an IO error + */ + @Throws(MissingArg::class, UnsupportedAsymmetricAlgo::class, IOException::class) + fun generate(): Ready +} From 8df4a520bdb6c2e37ea81cf71f0cf7e8b422eb7e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:20:29 +0100 Subject: [PATCH 027/238] Kotlin conversion: InlineDetach --- .../main/java/sop/operation/InlineDetach.java | 52 ------------------- .../main/kotlin/sop/operation/InlineDetach.kt | 43 +++++++++++++++ 2 files changed, 43 insertions(+), 52 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/InlineDetach.java create mode 100644 sop-java/src/main/kotlin/sop/operation/InlineDetach.kt diff --git a/sop-java/src/main/java/sop/operation/InlineDetach.java b/sop-java/src/main/java/sop/operation/InlineDetach.java deleted file mode 100644 index aba40b1..0000000 --- a/sop-java/src/main/java/sop/operation/InlineDetach.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; - -/** - * Split cleartext signed messages up into data and signatures. - */ -public interface InlineDetach { - - /** - * Do not wrap the signatures in ASCII armor. - * @return builder - */ - InlineDetach noArmor(); - - /** - * Detach the provided signed message from its signatures. - * - * @param messageInputStream input stream containing the signed message - * @return result containing the detached message - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the input stream does not contain a signed message - */ - ReadyWithResult message(InputStream messageInputStream) - throws IOException, - SOPGPException.BadData; - - /** - * Detach the provided cleartext signed message from its signatures. - * - * @param message byte array containing the signed message - * @return result containing the detached message - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.BadData if the byte array does not contain a signed message - */ - default ReadyWithResult message(byte[] message) - throws IOException, - SOPGPException.BadData { - return message(new ByteArrayInputStream(message)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt new file mode 100644 index 0000000..941a9bf --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.ReadyWithResult +import sop.Signatures +import sop.exception.SOPGPException.BadData + +interface InlineDetach { + + /** + * Do not wrap the signatures in ASCII armor. + * + * @return builder + */ + fun noArmor(): InlineDetach + + /** + * Detach the provided signed message from its signatures. + * + * @param messageInputStream input stream containing the signed message + * @return result containing the detached message + * @throws IOException in case of an IO error + * @throws BadData if the input stream does not contain a signed message + */ + @Throws(IOException::class, BadData::class) + fun message(messageInputStream: InputStream): ReadyWithResult + + /** + * Detach the provided cleartext signed message from its signatures. + * + * @param message byte array containing the signed message + * @return result containing the detached message + * @throws IOException in case of an IO error + * @throws BadData if the byte array does not contain a signed message + */ + @Throws(IOException::class, BadData::class) + fun message(message: ByteArray): ReadyWithResult = message(message.inputStream()) +} From 9283f81c56662d63c565dfd2aaac14868c4bbc3d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:22:12 +0100 Subject: [PATCH 028/238] Replace ByteArrayInputStream with inputStream() --- sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt | 4 +--- sop-java/src/main/kotlin/sop/operation/Encrypt.kt | 7 +++---- sop-java/src/main/kotlin/sop/operation/ExtractCert.kt | 3 +-- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt index 2dd9218..d899b54 100644 --- a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt +++ b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt @@ -4,7 +4,6 @@ package sop.operation -import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import sop.exception.SOPGPException.BadData @@ -31,6 +30,5 @@ interface DetachedVerify : AbstractVerify, VerifySignatures { * @throws IOException in case of an IO error */ @Throws(BadData::class, IOException::class) - fun signatures(signatures: ByteArray): VerifySignatures = - signatures(ByteArrayInputStream(signatures)) + fun signatures(signatures: ByteArray): VerifySignatures = signatures(signatures.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt index ad30149..0daebee 100644 --- a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -4,7 +4,6 @@ package sop.operation -import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import sop.Profile @@ -57,7 +56,7 @@ interface Encrypt { */ @Throws( KeyCannotSign::class, UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) - fun signWith(key: ByteArray): Encrypt = signWith(ByteArrayInputStream(key)) + fun signWith(key: ByteArray): Encrypt = signWith(key.inputStream()) /** * Provide the password for the secret key used for signing. @@ -125,7 +124,7 @@ interface Encrypt { UnsupportedAsymmetricAlgo::class, BadData::class, IOException::class) - fun withCert(cert: ByteArray): Encrypt = withCert(ByteArrayInputStream(cert)) + fun withCert(cert: ByteArray): Encrypt = withCert(cert.inputStream()) /** * Pass in a profile. @@ -162,5 +161,5 @@ interface Encrypt { * @throws KeyIsProtected if at least one signing key cannot be unlocked */ @Throws(IOException::class, KeyIsProtected::class) - fun plaintext(plaintext: ByteArray): Ready = plaintext(ByteArrayInputStream(plaintext)) + fun plaintext(plaintext: ByteArray): Ready = plaintext(plaintext.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt index bd48e08..e2ce1cc 100644 --- a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt +++ b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt @@ -4,7 +4,6 @@ package sop.operation -import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import sop.Ready @@ -38,5 +37,5 @@ interface ExtractCert { * @throws BadData if the byte array does not contain an OpenPGP key */ @Throws(IOException::class, BadData::class) - fun key(key: ByteArray): Ready = key(ByteArrayInputStream(key)) + fun key(key: ByteArray): Ready = key(key.inputStream()) } From be0ceb0886a9b6a51b51a4519b4098e761ad0cf3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:25:25 +0100 Subject: [PATCH 029/238] Kotlin conversion: InlineSign --- .../main/java/sop/operation/InlineSign.java | 60 ------------------- .../main/kotlin/sop/operation/InlineSign.kt | 47 +++++++++++++++ 2 files changed, 47 insertions(+), 60 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/InlineSign.java create mode 100644 sop-java/src/main/kotlin/sop/operation/InlineSign.kt diff --git a/sop-java/src/main/java/sop/operation/InlineSign.java b/sop-java/src/main/java/sop/operation/InlineSign.java deleted file mode 100644 index d45aebd..0000000 --- a/sop-java/src/main/java/sop/operation/InlineSign.java +++ /dev/null @@ -1,60 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; - -public interface InlineSign extends AbstractSign { - - /** - * Sets the signature mode. - * Note: This method has to be called before {@link #key(InputStream)} is called. - * - * @param mode signature mode - * @return builder instance - * - * @throws sop.exception.SOPGPException.UnsupportedOption if this option is not supported - */ - InlineSign mode(InlineSignAs mode) - throws SOPGPException.UnsupportedOption; - - /** - * Signs data. - * - * @param data input stream containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - Ready data(InputStream data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText; - - /** - * Signs data. - * - * @param data byte array containing data - * @return ready - * - * @throws IOException in case of an IO error - * @throws sop.exception.SOPGPException.KeyIsProtected if at least one signing key cannot be unlocked - * @throws sop.exception.SOPGPException.ExpectedText if text data was expected, but binary data was encountered - */ - default Ready data(byte[] data) - throws IOException, - SOPGPException.KeyIsProtected, - SOPGPException.ExpectedText { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt new file mode 100644 index 0000000..11b5668 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Ready +import sop.enums.InlineSignAs +import sop.exception.SOPGPException.* + +interface InlineSign : AbstractSign { + + /** + * Sets the signature mode. Note: This method has to be called before [.key] is called. + * + * @param mode signature mode + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun mode(mode: InlineSignAs): InlineSign + + /** + * Signs data. + * + * @param data input stream containing data + * @return ready + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + * @throws ExpectedText if text data was expected, but binary data was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: InputStream): Ready + + /** + * Signs data. + * + * @param data byte array containing data + * @return ready + * @throws IOException in case of an IO error + * @throws KeyIsProtected if at least one signing key cannot be unlocked + * @throws ExpectedText if text data was expected, but binary data was encountered + */ + @Throws(IOException::class, KeyIsProtected::class, ExpectedText::class) + fun data(data: ByteArray): Ready = data(data.inputStream()) +} From 6c14f249bb66ba8ca019c8235fe2c21112baf876 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:25:47 +0100 Subject: [PATCH 030/238] Kotlin conversion: InlineVerify --- .../main/java/sop/operation/InlineVerify.java | 54 ------------------- .../main/kotlin/sop/operation/InlineVerify.kt | 42 +++++++++++++++ 2 files changed, 42 insertions(+), 54 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/InlineVerify.java create mode 100644 sop-java/src/main/kotlin/sop/operation/InlineVerify.kt diff --git a/sop-java/src/main/java/sop/operation/InlineVerify.java b/sop-java/src/main/java/sop/operation/InlineVerify.java deleted file mode 100644 index ac662a0..0000000 --- a/sop-java/src/main/java/sop/operation/InlineVerify.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -/** - * API for verification of inline-signed messages. - */ -public interface InlineVerify extends AbstractVerify { - - /** - * Provide the inline-signed data. - * The result can be used to write the plaintext message out and to get the verifications. - * - * @param data signed data - * @return list of signature verifications - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - ReadyWithResult> data(InputStream data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData; - - /** - * Provide the inline-signed data. - * The result can be used to write the plaintext message out and to get the verifications. - * - * @param data signed data - * @return list of signature verifications - * - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default ReadyWithResult> data(byte[] data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt new file mode 100644 index 0000000..c16b269 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.ReadyWithResult +import sop.Verification +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.NoSignature + +/** API for verification of inline-signed messages. */ +interface InlineVerify : AbstractVerify { + + /** + * Provide the inline-signed data. The result can be used to write the plaintext message out and + * to get the verifications. + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: InputStream): ReadyWithResult> + + /** + * Provide the inline-signed data. The result can be used to write the plaintext message out and + * to get the verifications. + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: ByteArray): ReadyWithResult> = data(data.inputStream()) +} From 145cadef4f7557a52529eaea1a04eb3bcb484868 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:27:17 +0100 Subject: [PATCH 031/238] Kotlin conversion: ListProfiles --- .../main/java/sop/operation/ListProfiles.java | 43 ------------------- .../main/kotlin/sop/operation/ListProfiles.kt | 34 +++++++++++++++ 2 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/ListProfiles.java create mode 100644 sop-java/src/main/kotlin/sop/operation/ListProfiles.kt diff --git a/sop-java/src/main/java/sop/operation/ListProfiles.java b/sop-java/src/main/java/sop/operation/ListProfiles.java deleted file mode 100644 index 0c17bd6..0000000 --- a/sop-java/src/main/java/sop/operation/ListProfiles.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Profile; - -import java.util.List; - -/** - * Subcommand to list supported profiles of other subcommands. - */ -public interface ListProfiles { - - /** - * Provide the name of the subcommand for which profiles shall be listed. - * The returned list of profiles MUST NOT contain more than 4 entries. - * - * @param command command name (e.g.

generate-key
) - * @return list of profiles. - */ - List subcommand(String command); - - /** - * Return a list of {@link Profile Profiles} supported by the {@link GenerateKey} implementation. - * - * @return profiles - */ - default List generateKey() { - return subcommand("generate-key"); - } - - /** - * Return a list of {@link Profile Profiles} supported by the {@link Encrypt} implementation. - * - * @return profiles - */ - default List encrypt() { - return subcommand("encrypt"); - } - -} diff --git a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt new file mode 100644 index 0000000..315faf2 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Profile + +/** Subcommand to list supported profiles of other subcommands. */ +interface ListProfiles { + + /** + * Provide the name of the subcommand for which profiles shall be listed. The returned list of + * profiles MUST NOT contain more than 4 entries. + * + * @param command command name (e.g. `generate-key`) + * @return list of profiles. + */ + fun subcommand(command: String): List + + /** + * Return a list of [Profiles][Profile] supported by the [GenerateKey] implementation. + * + * @return profiles + */ + fun generateKey(): List = subcommand("generate-key") + + /** + * Return a list of [Profiles][Profile] supported by the [Encrypt] implementation. + * + * @return profiles + */ + fun encrypt(): List = subcommand("encrypt") +} From 0ee4638bebf88f28fc434f9c78b6fef45c8a877a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:28:59 +0100 Subject: [PATCH 032/238] Kotlin conversion: RevokeKey --- .../main/java/sop/operation/RevokeKey.java | 54 ------------------- .../main/kotlin/sop/operation/RevokeKey.kt | 48 +++++++++++++++++ 2 files changed, 48 insertions(+), 54 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/RevokeKey.java create mode 100644 sop-java/src/main/kotlin/sop/operation/RevokeKey.kt diff --git a/sop-java/src/main/java/sop/operation/RevokeKey.java b/sop-java/src/main/java/sop/operation/RevokeKey.java deleted file mode 100644 index 3ceb5b3..0000000 --- a/sop-java/src/main/java/sop/operation/RevokeKey.java +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.util.UTF8Util; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; - -public interface RevokeKey { - - /** - * Disable ASCII armor encoding. - * - * @return builder instance - */ - RevokeKey noArmor(); - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - default RevokeKey withKeyPassword(String password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable { - return withKeyPassword(password.getBytes(UTF8Util.UTF8)); - } - - /** - * Provide the decryption password for the secret key. - * - * @param password password - * @return builder instance - * @throws SOPGPException.UnsupportedOption if the implementation does not support key passwords - * @throws SOPGPException.PasswordNotHumanReadable if the password is not human-readable - */ - RevokeKey withKeyPassword(byte[] password) - throws SOPGPException.UnsupportedOption, - SOPGPException.PasswordNotHumanReadable; - - default Ready keys(byte[] bytes) { - return keys(new ByteArrayInputStream(bytes)); - } - - Ready keys(InputStream keys); -} diff --git a/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt new file mode 100644 index 0000000..f3cbe5c --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.InputStream +import sop.Ready +import sop.exception.SOPGPException.PasswordNotHumanReadable +import sop.exception.SOPGPException.UnsupportedOption +import sop.util.UTF8Util + +interface RevokeKey { + + /** + * Disable ASCII armor encoding. + * + * @return builder instance + */ + fun noArmor(): RevokeKey + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: String): RevokeKey = + withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + * @throws PasswordNotHumanReadable if the password is not human-readable + */ + @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + fun withKeyPassword(password: ByteArray): RevokeKey + + fun keys(bytes: ByteArray): Ready = keys(bytes.inputStream()) + + fun keys(keys: InputStream): Ready +} From a8c2e72ef589bdeea16df71c2901842b6fdd5220 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:30:24 +0100 Subject: [PATCH 033/238] Kotlin conversion: VerifySignatures --- .../java/sop/operation/VerifySignatures.java | 46 ------------------- .../kotlin/sop/operation/VerifySignatures.kt | 38 +++++++++++++++ 2 files changed, 38 insertions(+), 46 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/VerifySignatures.java create mode 100644 sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt diff --git a/sop-java/src/main/java/sop/operation/VerifySignatures.java b/sop-java/src/main/java/sop/operation/VerifySignatures.java deleted file mode 100644 index 5181514..0000000 --- a/sop-java/src/main/java/sop/operation/VerifySignatures.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -import sop.Verification; -import sop.exception.SOPGPException; - -public interface VerifySignatures { - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no valid signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - List data(InputStream data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData; - - /** - * Provide the signed data (without signatures). - * - * @param data signed data - * @return list of signature verifications - * @throws IOException in case of an IO error - * @throws SOPGPException.NoSignature when no valid signature is found - * @throws SOPGPException.BadData when the data is invalid OpenPGP data - */ - default List data(byte[] data) - throws IOException, - SOPGPException.NoSignature, - SOPGPException.BadData { - return data(new ByteArrayInputStream(data)); - } -} diff --git a/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt new file mode 100644 index 0000000..b75e4a5 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import java.io.IOException +import java.io.InputStream +import sop.Verification +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.NoSignature + +interface VerifySignatures { + + /** + * Provide the signed data (without signatures). + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no valid signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: InputStream): List + + /** + * Provide the signed data (without signatures). + * + * @param data signed data + * @return list of signature verifications + * @throws IOException in case of an IO error + * @throws NoSignature when no valid signature is found + * @throws BadData when the data is invalid OpenPGP data + */ + @Throws(IOException::class, NoSignature::class, BadData::class) + fun data(data: ByteArray): List = data(data.inputStream()) +} From d0ee9c2066ce0d72cbbd5fb9bcbdf09fbaaa3f17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:38:34 +0100 Subject: [PATCH 034/238] Kotlin conversion: Version --- .../src/main/java/sop/operation/Version.java | 109 ------------------ .../main/java/sop/operation/package-info.java | 9 -- .../src/main/kotlin/sop/operation/Version.kt | 100 ++++++++++++++++ 3 files changed, 100 insertions(+), 118 deletions(-) delete mode 100644 sop-java/src/main/java/sop/operation/Version.java delete mode 100644 sop-java/src/main/java/sop/operation/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/operation/Version.kt diff --git a/sop-java/src/main/java/sop/operation/Version.java b/sop-java/src/main/java/sop/operation/Version.java deleted file mode 100644 index b6d66b9..0000000 --- a/sop-java/src/main/java/sop/operation/Version.java +++ /dev/null @@ -1,109 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.operation; - -public interface Version { - - /** - * Return the implementations name. - * e.g. "SOP", - * - * @return implementation name - */ - String getName(); - - /** - * Return the implementations short version string. - * e.g. "1.0" - * - * @return version string - */ - String getVersion(); - - /** - * Return version information about the used OpenPGP backend. - * e.g. "Bouncycastle 1.70" - * - * @return backend version string - */ - String getBackendVersion(); - - /** - * Return an extended version string containing multiple lines of version information. - * The first line MUST match the information produced by {@link #getName()} and {@link #getVersion()}, but the rest of the text - * has no defined structure. - * Example: - *
-     *     "SOP 1.0
-     *     Awesome PGP!
-     *     Using Bouncycastle 1.70
-     *     LibFoo 1.2.2
-     *     See https://pgp.example.org/sop/ for more information"
-     * 
- * - * @return extended version string - */ - String getExtendedVersion(); - - /** - * Return the revision of the SOP specification that this implementation is implementing, for example, - *
draft-dkg-openpgp-stateless-cli-06
. - * If the implementation targets a specific draft but the implementer knows the implementation is incomplete, - * it should prefix the draft title with a "~" (TILDE, U+007E), for example: - *
~draft-dkg-openpgp-stateless-cli-06
. - * The implementation MAY emit additional text about its relationship to the targeted draft on the lines following - * the versioned title. - * - * @return implemented SOP spec version - */ - default String getSopSpecVersion() { - StringBuilder sb = new StringBuilder(); - if (isSopSpecImplementationIncomplete()) { - sb.append('~'); - } - - sb.append(getSopSpecRevisionName()); - - if (getSopSpecImplementationRemarks() != null) { - sb.append('\n') - .append('\n') - .append(getSopSpecImplementationRemarks()); - } - - return sb.toString(); - } - - /** - * Return the version number of the latest targeted SOP spec revision. - * - * @return SOP spec revision number - */ - int getSopSpecRevisionNumber(); - - /** - * Return the name of the latest targeted revision of the SOP spec. - * - * @return SOP spec revision string - */ - default String getSopSpecRevisionName() { - return "draft-dkg-openpgp-stateless-cli-" + String.format("%02d", getSopSpecRevisionNumber()); - } - - /** - * Return
true
, if this implementation of the SOP spec is known to be incomplete or defective. - * - * @return true if incomplete, false otherwise - */ - boolean isSopSpecImplementationIncomplete(); - - /** - * Return free-form text containing remarks about the completeness of the SOP implementation. - * If there are no remarks, this method returns
null
. - * - * @return remarks or null - */ - String getSopSpecImplementationRemarks(); - -} diff --git a/sop-java/src/main/java/sop/operation/package-info.java b/sop-java/src/main/java/sop/operation/package-info.java deleted file mode 100644 index dde4d5b..0000000 --- a/sop-java/src/main/java/sop/operation/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Different cryptographic operations. - */ -package sop.operation; diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt new file mode 100644 index 0000000..9b3bd8a --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +interface Version { + + /** + * Return the implementations name. e.g. `SOP`, + * + * @return implementation name + */ + fun getName(): String + + /** + * Return the implementations short version string. e.g. `1.0` + * + * @return version string + */ + fun getVersion(): String + + /** + * Return version information about the used OpenPGP backend. e.g. `Bouncycastle 1.70` + * + * @return backend version string + */ + fun getBackendVersion(): String + + /** + * Return an extended version string containing multiple lines of version information. The first + * line MUST match the information produced by [getName] and [getVersion], but the rest of the + * text has no defined structure. Example: + * ``` + * "SOP 1.0 + * Awesome PGP! + * Using Bouncycastle 1.70 + * LibFoo 1.2.2 + * See https://pgp.example.org/sop/ for more information" + * ``` + * + * @return extended version string + */ + fun getExtendedVersion(): String + + /** + * Return the revision of the SOP specification that this implementation is implementing, for + * example, `draft-dkg-openpgp-stateless-cli-06`. If the implementation targets a specific draft + * but the implementer knows the implementation is incomplete, it should prefix the draft title + * with a `~` (TILDE, U+007E), for example: `~draft-dkg-openpgp-stateless-cli-06`. The + * implementation MAY emit additional text about its relationship to the targeted draft on the + * lines following the versioned title. + * + * @return implemented SOP spec version + */ + fun getSopSpecVersion(): String { + return buildString { + if (isSopSpecImplementationIncomplete()) append('~') + append(getSopSpecRevisionName()) + if (getSopSpecImplementationRemarks() != null) { + append('\n') + append('\n') + append(getSopSpecImplementationRemarks()) + } + } + } + + /** + * Return the version number of the latest targeted SOP spec revision. + * + * @return SOP spec revision number + */ + fun getSopSpecRevisionNumber(): Int + + /** + * Return the name of the latest targeted revision of the SOP spec. + * + * @return SOP spec revision string + */ + fun getSopSpecRevisionName(): String = buildString { + append("draft-dkg-openpgp-stateless-cli-") + append(String.format("%02d", getSopSpecRevisionNumber())) + } + + /** + * Return
true
, if this implementation of the SOP spec is known to be incomplete or + * defective. + * + * @return true if incomplete, false otherwise + */ + fun isSopSpecImplementationIncomplete(): Boolean + + /** + * Return free-form text containing remarks about the completeness of the SOP implementation. If + * there are no remarks, this method returns
null
. + * + * @return remarks or null + */ + fun getSopSpecImplementationRemarks(): String? +} From 049c18c17ba3907057c65f39dbfa5bf57a0f2960 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:45:56 +0100 Subject: [PATCH 035/238] Fix sop-java-picocli jar task --- sop-java-picocli/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 664c385..438ef50 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -38,6 +38,7 @@ application { jar { dependsOn(":sop-java:jar") + duplicatesStrategy(DuplicatesStrategy.EXCLUDE) manifest { attributes 'Main-Class': "$mainClassName" From 4b9e2c206f18bcec162bb3a82674c151a61239fc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:46:41 +0100 Subject: [PATCH 036/238] Fix DecryptionResult constructor --- sop-java/src/main/kotlin/sop/DecryptionResult.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/DecryptionResult.kt b/sop-java/src/main/kotlin/sop/DecryptionResult.kt index 1ba98bb..b653297 100644 --- a/sop-java/src/main/kotlin/sop/DecryptionResult.kt +++ b/sop-java/src/main/kotlin/sop/DecryptionResult.kt @@ -6,11 +6,10 @@ package sop import sop.util.Optional -data class DecryptionResult -internal constructor(val sessionKey: Optional, val verifications: List) { +class DecryptionResult(sessionKey: SessionKey?, val verifications: List) { + val sessionKey: Optional - constructor( - sessionKey: SessionKey?, - verifications: List - ) : this(Optional.ofNullable(sessionKey), verifications) + init { + this.sessionKey = Optional.ofNullable(sessionKey) + } } From d5c0d4e390ccd0b0c0171c621eed497d8c897a3b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:47:14 +0100 Subject: [PATCH 037/238] Kotlin conversion: ArmorLabel --- .../src/main/java/sop/enums/ArmorLabel.java | 19 ------------------- .../src/main/kotlin/sop/enums/ArmorLabel.kt | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 19 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/ArmorLabel.java create mode 100644 sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt diff --git a/sop-java/src/main/java/sop/enums/ArmorLabel.java b/sop-java/src/main/java/sop/enums/ArmorLabel.java deleted file mode 100644 index bb97e84..0000000 --- a/sop-java/src/main/java/sop/enums/ArmorLabel.java +++ /dev/null @@ -1,19 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum ArmorLabel { - Auto, - Sig, - Key, - Cert, - Message, - ; - - @Override - public String toString() { - return super.toString().toLowerCase(); - } -} diff --git a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt b/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt new file mode 100644 index 0000000..8b4e2cd --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +@Deprecated("Use of armor labels is deprecated.") +enum class ArmorLabel { + auto, + sig, + key, + cert, + message +} From 1c290e0c8f4cae89108adcae5d79334513c49822 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:48:48 +0100 Subject: [PATCH 038/238] Kotlin conversion: EncryptAs --- sop-java/src/main/java/sop/enums/EncryptAs.java | 16 ---------------- sop-java/src/main/kotlin/sop/enums/EncryptAs.kt | 10 ++++++++++ .../testsuite/operation/EncryptDecryptTest.java | 4 ++-- 3 files changed, 12 insertions(+), 18 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/EncryptAs.java create mode 100644 sop-java/src/main/kotlin/sop/enums/EncryptAs.kt diff --git a/sop-java/src/main/java/sop/enums/EncryptAs.java b/sop-java/src/main/java/sop/enums/EncryptAs.java deleted file mode 100644 index 7e7d4d1..0000000 --- a/sop-java/src/main/java/sop/enums/EncryptAs.java +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum EncryptAs { - Binary, - Text, - ; - - @Override - public String toString() { - return super.toString().toLowerCase(); - } -} diff --git a/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt b/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt new file mode 100644 index 0000000..0b6aa8e --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/EncryptAs.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class EncryptAs { + binary, + text +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java index 0c382dc..9138f64 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -142,7 +142,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .mode(EncryptAs.Binary) + .mode(EncryptAs.binary) .plaintext(message) .getBytes(); @@ -173,7 +173,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .mode(EncryptAs.Text) + .mode(EncryptAs.text) .plaintext(message) .getBytes(); From be6be3deac79a922f24285d45b024438b22b3b04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:50:09 +0100 Subject: [PATCH 039/238] Kotlin conversion: InlineSignAs --- .../src/main/java/sop/enums/InlineSignAs.java | 24 ------------------- .../src/main/kotlin/sop/enums/InlineSignAs.kt | 17 +++++++++++++ 2 files changed, 17 insertions(+), 24 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/InlineSignAs.java create mode 100644 sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt diff --git a/sop-java/src/main/java/sop/enums/InlineSignAs.java b/sop-java/src/main/java/sop/enums/InlineSignAs.java deleted file mode 100644 index c1097df..0000000 --- a/sop-java/src/main/java/sop/enums/InlineSignAs.java +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum InlineSignAs { - - /** - * Signature is made over the binary message. - */ - binary, - - /** - * Signature is made over the message in text mode. - */ - text, - - /** - * Signature is made using the Cleartext Signature Framework. - */ - clearsigned, -} - diff --git a/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt b/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt new file mode 100644 index 0000000..3440edf --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/InlineSignAs.kt @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class InlineSignAs { + + /** Signature is made over the binary message. */ + binary, + + /** Signature is made over the message in text mode. */ + text, + + /** Signature is made using the Cleartext Signature Framework. */ + clearsigned +} From 30c369d24a10b50b64064963b643e0cd203efec9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:51:16 +0100 Subject: [PATCH 040/238] Kotlin conversion: SignAs --- sop-java/src/main/java/sop/enums/SignAs.java | 23 ------------------- sop-java/src/main/kotlin/sop/enums/SignAs.kt | 12 ++++++++++ .../DetachedSignDetachedVerifyTest.java | 2 +- 3 files changed, 13 insertions(+), 24 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/SignAs.java create mode 100644 sop-java/src/main/kotlin/sop/enums/SignAs.kt diff --git a/sop-java/src/main/java/sop/enums/SignAs.java b/sop-java/src/main/java/sop/enums/SignAs.java deleted file mode 100644 index 1174098..0000000 --- a/sop-java/src/main/java/sop/enums/SignAs.java +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -public enum SignAs { - /** - * Signature is made over the binary message. - */ - Binary, - - /** - * Signature is made over the message in text mode. - */ - Text, - ; - - @Override - public String toString() { - return super.toString().toLowerCase(); - } -} diff --git a/sop-java/src/main/kotlin/sop/enums/SignAs.kt b/sop-java/src/main/kotlin/sop/enums/SignAs.kt new file mode 100644 index 0000000..832e831 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/SignAs.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +enum class SignAs { + /** Signature is made over the binary message. */ + binary, + /** Signature is made over the message in text mode. */ + text +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java index e715c14..e404599 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java @@ -62,7 +62,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] signature = sop.detachedSign() .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) - .mode(SignAs.Text) + .mode(SignAs.text) .data(message) .toByteArrayAndResult() .getBytes(); From 01f98df80b4f5e6757b887fe99b3c1efc21ceb10 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 14:52:45 +0100 Subject: [PATCH 041/238] Kotlin conversion: SignatureMode --- .../main/java/sop/enums/SignatureMode.java | 25 ------------------- .../src/main/java/sop/enums/package-info.java | 9 ------- .../main/kotlin/sop/enums/SignatureMode.kt | 18 +++++++++++++ 3 files changed, 18 insertions(+), 34 deletions(-) delete mode 100644 sop-java/src/main/java/sop/enums/SignatureMode.java delete mode 100644 sop-java/src/main/java/sop/enums/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/enums/SignatureMode.kt diff --git a/sop-java/src/main/java/sop/enums/SignatureMode.java b/sop-java/src/main/java/sop/enums/SignatureMode.java deleted file mode 100644 index 71ce7d8..0000000 --- a/sop-java/src/main/java/sop/enums/SignatureMode.java +++ /dev/null @@ -1,25 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums; - -/** - * Enum referencing relevant signature types. - * - * @see - * RFC4880 §5.2.1 - Signature Types - */ -public enum SignatureMode { - /** - * Signature of a binary document (
0x00
). - */ - binary, - - /** - * Signature of a canonical text document (
0x01
). - */ - text - - // Other Signature Types are irrelevant. -} diff --git a/sop-java/src/main/java/sop/enums/package-info.java b/sop-java/src/main/java/sop/enums/package-info.java deleted file mode 100644 index 67148d3..0000000 --- a/sop-java/src/main/java/sop/enums/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Enumerations. - */ -package sop.enums; diff --git a/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt b/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt new file mode 100644 index 0000000..7213195 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/enums/SignatureMode.kt @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.enums + +/** + * Enum referencing relevant signature types. + * + * @see RFC4880 §5.2.1 - Signature + * Types + */ +enum class SignatureMode { + /** Signature of a binary document (type `0x00`). */ + binary, + /** Signature of a canonical text document (type `0x01`). */ + text +} From b7007cc0070659c58e0e9cad520acd58b6b07342 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:00:21 +0100 Subject: [PATCH 042/238] Kotlin conversion: HexUtil --- sop-java/src/main/java/sop/util/HexUtil.java | 47 -------------------- sop-java/src/main/kotlin/sop/util/HexUtil.kt | 38 ++++++++++++++++ 2 files changed, 38 insertions(+), 47 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/HexUtil.java create mode 100644 sop-java/src/main/kotlin/sop/util/HexUtil.kt diff --git a/sop-java/src/main/java/sop/util/HexUtil.java b/sop-java/src/main/java/sop/util/HexUtil.java deleted file mode 100644 index 9b88f53..0000000 --- a/sop-java/src/main/java/sop/util/HexUtil.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2021 Paul Schaub, @maybeWeCouldStealAVan, @Dave L. -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -public class HexUtil { - - private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); - - /** - * Encode a byte array to a hex string. - * - * @see - * How to convert a byte array to a hex string in Java? - * @param bytes bytes - * @return hex encoding - */ - public static String bytesToHex(byte[] bytes) { - char[] hexChars = new char[bytes.length * 2]; - for (int j = 0; j < bytes.length; j++) { - int v = bytes[j] & 0xFF; - hexChars[j * 2] = HEX_ARRAY[v >>> 4]; - hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; - } - return new String(hexChars); - } - - /** - * Decode a hex string into a byte array. - * - * @see - * Convert a string representation of a hex dump to a byte array using Java? - * @param s hex string - * @return decoded byte array - */ - public static byte[] hexToBytes(String s) { - int len = s.length(); - byte[] data = new byte[len / 2]; - for (int i = 0; i < len; i += 2) { - data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) - + Character.digit(s.charAt(i + 1), 16)); - } - return data; - } -} diff --git a/sop-java/src/main/kotlin/sop/util/HexUtil.kt b/sop-java/src/main/kotlin/sop/util/HexUtil.kt new file mode 100644 index 0000000..f372137 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/HexUtil.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +class HexUtil { + + companion object { + /** + * Encode a byte array to a hex string. + * + * @param bytes bytes + * @return hex encoding + * @see + * [Convert Byte Arrays to Hex Strings in Kotlin](https://www.baeldung.com/kotlin/byte-arrays-to-hex-strings) + */ + @JvmStatic fun bytesToHex(bytes: ByteArray): String = bytes.toHex() + + /** + * Decode a hex string into a byte array. + * + * @param s hex string + * @return decoded byte array + * @see + * [Kotlin convert hex string to ByteArray](https://stackoverflow.com/a/66614516/11150851) + */ + @JvmStatic fun hexToBytes(s: String): ByteArray = s.decodeHex() + } +} + +fun String.decodeHex(): ByteArray { + check(length % 2 == 0) { "Hex encoding must have even number of digits." } + + return chunked(2).map { it.toInt(16).toByte() }.toByteArray() +} + +fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02X".format(eachByte) } From 05886228df497c18ce94ffd756fc2075e48b0a95 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:07:13 +0100 Subject: [PATCH 043/238] Kotlin conversion: ProxyOutputStream --- .../main/java/sop/util/ProxyOutputStream.java | 80 ------------------- .../main/kotlin/sop/util/ProxyOutputStream.kt | 75 +++++++++++++++++ 2 files changed, 75 insertions(+), 80 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/ProxyOutputStream.java create mode 100644 sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt diff --git a/sop-java/src/main/java/sop/util/ProxyOutputStream.java b/sop-java/src/main/java/sop/util/ProxyOutputStream.java deleted file mode 100644 index ed24fc2..0000000 --- a/sop-java/src/main/java/sop/util/ProxyOutputStream.java +++ /dev/null @@ -1,80 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.OutputStream; - -/** - * {@link OutputStream} that buffers data being written into it, until its underlying output stream is being replaced. - * At that point, first all the buffered data is being written to the underlying stream, followed by any successive - * data that may get written to the {@link ProxyOutputStream}. - *

- * This class is useful if we need to provide an {@link OutputStream} at one point in time when the final - * target output stream is not yet known. - */ -public class ProxyOutputStream extends OutputStream { - - private final ByteArrayOutputStream buffer; - private OutputStream swapped; - - public ProxyOutputStream() { - this.buffer = new ByteArrayOutputStream(); - } - - public synchronized void replaceOutputStream(OutputStream underlying) throws IOException { - if (underlying == null) { - throw new NullPointerException("Underlying OutputStream cannot be null."); - } - this.swapped = underlying; - - byte[] bufferBytes = buffer.toByteArray(); - swapped.write(bufferBytes); - } - - @Override - public synchronized void write(byte[] b) throws IOException { - if (swapped == null) { - buffer.write(b); - } else { - swapped.write(b); - } - } - - @Override - public synchronized void write(byte[] b, int off, int len) throws IOException { - if (swapped == null) { - buffer.write(b, off, len); - } else { - swapped.write(b, off, len); - } - } - - @Override - public synchronized void flush() throws IOException { - buffer.flush(); - if (swapped != null) { - swapped.flush(); - } - } - - @Override - public synchronized void close() throws IOException { - buffer.close(); - if (swapped != null) { - swapped.close(); - } - } - - @Override - public synchronized void write(int i) throws IOException { - if (swapped == null) { - buffer.write(i); - } else { - swapped.write(i); - } - } -} diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt new file mode 100644 index 0000000..142f7b3 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream + +/** + * [OutputStream] that buffers data being written into it, until its underlying output stream is + * being replaced. At that point, first all the buffered data is being written to the underlying + * stream, followed by any successive data that may get written to the [ProxyOutputStream]. This + * 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. + */ +class ProxyOutputStream { + private val buffer = ByteArrayOutputStream() + private var swapped: OutputStream? = null + + @Synchronized + fun replaceOutputStream(underlying: OutputStream) { + this.swapped = underlying + swapped!!.write(buffer.toByteArray()) + } + + @Synchronized + @Throws(IOException::class) + fun write(b: ByteArray) { + if (swapped == null) { + buffer.write(b) + } else { + swapped!!.write(b) + } + } + + @Synchronized + @Throws(IOException::class) + fun write(b: ByteArray, off: Int, len: Int) { + if (swapped == null) { + buffer.write(b, off, len) + } else { + swapped!!.write(b, off, len) + } + } + + @Synchronized + @Throws(IOException::class) + fun flush() { + buffer.flush() + if (swapped != null) { + swapped!!.flush() + } + } + + @Synchronized + @Throws(IOException::class) + fun close() { + buffer.close() + if (swapped != null) { + swapped!!.close() + } + } + + @Synchronized + @Throws(IOException::class) + fun write(i: Int) { + if (swapped == null) { + buffer.write(i) + } else { + swapped!!.write(i) + } + } +} From 25a33611fdab6eba7cb3625677d37ccd5c16fd7d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:10:46 +0100 Subject: [PATCH 044/238] Kotlin conversion: UTCUtil --- sop-java/src/main/java/sop/util/UTCUtil.java | 65 -------------------- sop-java/src/main/kotlin/sop/util/UTCUtil.kt | 62 +++++++++++++++++++ 2 files changed, 62 insertions(+), 65 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/UTCUtil.java create mode 100644 sop-java/src/main/kotlin/sop/util/UTCUtil.kt diff --git a/sop-java/src/main/java/sop/util/UTCUtil.java b/sop-java/src/main/java/sop/util/UTCUtil.java deleted file mode 100644 index 96a6b9c..0000000 --- a/sop-java/src/main/java/sop/util/UTCUtil.java +++ /dev/null @@ -1,65 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import javax.annotation.Nonnull; -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.TimeZone; - -/** - * Utility class to parse and format dates as ISO-8601 UTC timestamps. - */ -public class UTCUtil { - - public static final SimpleDateFormat UTC_FORMATTER = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); - public static final SimpleDateFormat[] UTC_PARSERS = new SimpleDateFormat[] { - UTC_FORMATTER, - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), - new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), - new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'") - }; - - static { - for (SimpleDateFormat f : UTC_PARSERS) { - f.setTimeZone(TimeZone.getTimeZone("UTC")); - } - } - /** - * Parse an ISO-8601 UTC timestamp from a string. - * - * @param dateString string - * @return date - * @throws ParseException if the date string is malformed and cannot be parsed - */ - @Nonnull - public static Date parseUTCDate(String dateString) throws ParseException { - ParseException exception = null; - for (SimpleDateFormat parser : UTC_PARSERS) { - try { - return parser.parse(dateString); - } catch (ParseException e) { - // Store first exception (that of UTC_FORMATTER) to throw if we fail to parse the date - if (exception == null) { - exception = e; - } - // Try next parser - } - } - // No parser worked, so we throw the store exception - throw exception; - } - - /** - * Format a date as ISO-8601 UTC timestamp. - * - * @param date date - * @return timestamp string - */ - public static String formatUTCDate(Date date) { - return UTC_FORMATTER.format(date); - } -} diff --git a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt new file mode 100644 index 0000000..8924f25 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.* + +class UTCUtil { + + companion object { + + @JvmStatic val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + @JvmStatic + val UTC_PARSERS = + arrayOf( + UTC_FORMATTER, + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX"), + SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'"), + SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")) + .onEach { fmt -> fmt.timeZone = TimeZone.getTimeZone("UTC") } + + /** + * Parse an ISO-8601 UTC timestamp from a string. + * + * @param dateString string + * @return date + * @throws ParseException if the date string is malformed and cannot be parsed + */ + @JvmStatic + @Throws(ParseException::class) + fun parseUTCDate(dateString: String): Date { + var exception: ParseException? = null + for (parser in UTC_PARSERS) { + try { + return parser.parse(dateString) + } catch (e: ParseException) { + // Store first exception (that of UTC_FORMATTER) to throw if we fail to parse + // the date + if (exception == null) { + exception = e + } + // Try next parser + } + } + throw exception!! + } + + /** + * Format a date as ISO-8601 UTC timestamp. + * + * @param date date + * @return timestamp string + */ + @JvmStatic + fun formatUTCDate(date: Date): String { + return UTC_FORMATTER.format(date) + } + } +} From e1a6ffd07a7e081e207f84d105344013f9535b17 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:18:39 +0100 Subject: [PATCH 045/238] Use @JvmField annotation --- sop-java/src/main/kotlin/sop/util/UTCUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt index 8924f25..4ae13bc 100644 --- a/sop-java/src/main/kotlin/sop/util/UTCUtil.kt +++ b/sop-java/src/main/kotlin/sop/util/UTCUtil.kt @@ -12,8 +12,8 @@ class UTCUtil { companion object { - @JvmStatic val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") - @JvmStatic + @JvmField val UTC_FORMATTER = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + @JvmField val UTC_PARSERS = arrayOf( UTC_FORMATTER, From 94b428ef62cc9735fad20cca4560eb14af6a2b41 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:18:48 +0100 Subject: [PATCH 046/238] Kotlin conversion: UTF8Util --- sop-java/src/main/java/sop/util/UTF8Util.java | 37 ------------------- .../src/main/java/sop/util/package-info.java | 8 ---- sop-java/src/main/kotlin/sop/util/UTF8Util.kt | 37 +++++++++++++++++++ 3 files changed, 37 insertions(+), 45 deletions(-) delete mode 100644 sop-java/src/main/java/sop/util/UTF8Util.java delete mode 100644 sop-java/src/main/java/sop/util/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/util/UTF8Util.kt diff --git a/sop-java/src/main/java/sop/util/UTF8Util.java b/sop-java/src/main/java/sop/util/UTF8Util.java deleted file mode 100644 index 1b4941b..0000000 --- a/sop-java/src/main/java/sop/util/UTF8Util.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.CharacterCodingException; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; -import java.nio.charset.CodingErrorAction; - -public class UTF8Util { - - public static final Charset UTF8 = Charset.forName("UTF8"); - private static final CharsetDecoder UTF8Decoder = UTF8 - .newDecoder() - .onUnmappableCharacter(CodingErrorAction.REPORT) - .onMalformedInput(CodingErrorAction.REPORT); - - /** - * Detect non-valid UTF8 data. - * - * @see ante on StackOverflow - * @param data utf-8 encoded bytes - * - * @return decoded string - * @throws CharacterCodingException if the input data does not resemble UTF8 - */ - public static String decodeUTF8(byte[] data) - throws CharacterCodingException { - ByteBuffer byteBuffer = ByteBuffer.wrap(data); - CharBuffer charBuffer = UTF8Decoder.decode(byteBuffer); - return charBuffer.toString(); - } -} diff --git a/sop-java/src/main/java/sop/util/package-info.java b/sop-java/src/main/java/sop/util/package-info.java deleted file mode 100644 index 3dd9fc1..0000000 --- a/sop-java/src/main/java/sop/util/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Utility classes. - */ -package sop.util; diff --git a/sop-java/src/main/kotlin/sop/util/UTF8Util.kt b/sop-java/src/main/kotlin/sop/util/UTF8Util.kt new file mode 100644 index 0000000..770f32c --- /dev/null +++ b/sop-java/src/main/kotlin/sop/util/UTF8Util.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.util + +import java.nio.ByteBuffer +import java.nio.charset.Charset +import java.nio.charset.CodingErrorAction + +class UTF8Util { + companion object { + @JvmField val UTF8: Charset = Charset.forName("UTF8") + + @JvmStatic + private val UTF8Decoder = + UTF8.newDecoder() + .onUnmappableCharacter(CodingErrorAction.REPORT) + .onMalformedInput(CodingErrorAction.REPORT) + + /** + * Detect non-valid UTF8 data. + * + * @param data utf-8 encoded bytes + * @return decoded string + * @throws CharacterCodingException if the input data does not resemble UTF8 + * @see [ante on StackOverflow](https://stackoverflow.com/a/1471193) + */ + @JvmStatic + @Throws(CharacterCodingException::class) + fun decodeUTF8(data: ByteArray): String { + val byteBuffer = ByteBuffer.wrap(data) + val charBuffer = UTF8Decoder.decode(byteBuffer) + return charBuffer.toString() + } + } +} From 7824ee92c536f990de01c63d07c2dbfd68f4f579 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:27:47 +0100 Subject: [PATCH 047/238] Kotlin conversion: SOPGPException --- .../java/sop/exception/SOPGPException.java | 473 ------------------ .../main/java/sop/exception/package-info.java | 9 - sop-java/src/main/java/sop/package-info.java | 8 - .../kotlin/sop/exception/SOPGPException.kt | 308 ++++++++++++ 4 files changed, 308 insertions(+), 490 deletions(-) delete mode 100644 sop-java/src/main/java/sop/exception/SOPGPException.java delete mode 100644 sop-java/src/main/java/sop/exception/package-info.java delete mode 100644 sop-java/src/main/java/sop/package-info.java create mode 100644 sop-java/src/main/kotlin/sop/exception/SOPGPException.kt diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java deleted file mode 100644 index 1d13065..0000000 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ /dev/null @@ -1,473 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.exception; - -public abstract class SOPGPException extends RuntimeException { - - public SOPGPException() { - super(); - } - - public SOPGPException(String message) { - super(message); - } - - public SOPGPException(Throwable e) { - super(e); - } - - public SOPGPException(String message, Throwable cause) { - super(message, cause); - } - - public abstract int getExitCode(); - - /** - * No acceptable signatures found (sop verify, inline-verify). - */ - public static class NoSignature extends SOPGPException { - - public static final int EXIT_CODE = 3; - - public NoSignature() { - this("No verifiable signature found."); - } - - public NoSignature(String message) { - super(message); - } - - public NoSignature(String errorMsg, NoSignature e) { - super(errorMsg, e); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Asymmetric algorithm unsupported (sop encrypt, sign, inline-sign). - */ - public static class UnsupportedAsymmetricAlgo extends SOPGPException { - - public static final int EXIT_CODE = 13; - - public UnsupportedAsymmetricAlgo(String message, Throwable e) { - super(message, e); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). - */ - public static class CertCannotEncrypt extends SOPGPException { - public static final int EXIT_CODE = 17; - - public CertCannotEncrypt(String message, Throwable cause) { - super(message, cause); - } - - public CertCannotEncrypt(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Missing required argument. - */ - public static class MissingArg extends SOPGPException { - - public static final int EXIT_CODE = 19; - - public MissingArg() { - - } - - public MissingArg(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Incomplete verification instructions (sop decrypt). - */ - public static class IncompleteVerification extends SOPGPException { - - public static final int EXIT_CODE = 23; - - public IncompleteVerification(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unable to decrypt (sop decrypt). - */ - public static class CannotDecrypt extends SOPGPException { - - public static final int EXIT_CODE = 29; - - public CannotDecrypt() { - - } - - public CannotDecrypt(String errorMsg, Throwable e) { - super(errorMsg, e); - } - - public CannotDecrypt(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Non-UTF-8 or otherwise unreliable password (sop encrypt). - */ - public static class PasswordNotHumanReadable extends SOPGPException { - - public static final int EXIT_CODE = 31; - - public PasswordNotHumanReadable() { - super(); - } - - public PasswordNotHumanReadable(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unsupported option. - */ - public static class UnsupportedOption extends SOPGPException { - - public static final int EXIT_CODE = 37; - - public UnsupportedOption(String message) { - super(message); - } - - public UnsupportedOption(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Invalid data type (no secret key where KEYS expected, etc.). - */ - public static class BadData extends SOPGPException { - - public static final int EXIT_CODE = 41; - - public BadData(String message) { - super(message); - } - - public BadData(Throwable throwable) { - super(throwable); - } - - public BadData(String message, Throwable throwable) { - super(message, throwable); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Non-Text input where text expected. - */ - public static class ExpectedText extends SOPGPException { - - public static final int EXIT_CODE = 53; - - public ExpectedText() { - super(); - } - - public ExpectedText(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Output file already exists. - */ - public static class OutputExists extends SOPGPException { - - public static final int EXIT_CODE = 59; - - public OutputExists(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Input file does not exist. - */ - public static class MissingInput extends SOPGPException { - - public static final int EXIT_CODE = 61; - - public MissingInput(String message, Throwable cause) { - super(message, cause); - } - - public MissingInput(String errorMsg) { - super(errorMsg); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * A KEYS input is protected (locked) with a password and sop failed to unlock it. - */ - public static class KeyIsProtected extends SOPGPException { - - public static final int EXIT_CODE = 67; - - public KeyIsProtected() { - super(); - } - - public KeyIsProtected(String message) { - super(message); - } - - public KeyIsProtected(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Unsupported subcommand. - */ - public static class UnsupportedSubcommand extends SOPGPException { - - public static final int EXIT_CODE = 69; - - public UnsupportedSubcommand(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * An indirect parameter is a special designator (it starts with @), but sop does not know how to handle the prefix. - */ - public static class UnsupportedSpecialPrefix extends SOPGPException { - - public static final int EXIT_CODE = 71; - - public UnsupportedSpecialPrefix(String errorMsg) { - super(errorMsg); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Exception that gets thrown if a special designator (starting with @) is given, but the filesystem contains - * a file matching the designator. - *

- * E.g.

@ENV:FOO
is given, but
./@ENV:FOO
exists on the filesystem. - */ - public static class AmbiguousInput extends SOPGPException { - - public static final int EXIT_CODE = 73; - - public AmbiguousInput(String message) { - super(message); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * Key not signature-capable (e.g. expired, revoked, unacceptable usage flags). - */ - public static class KeyCannotSign extends SOPGPException { - - public static final int EXIT_CODE = 79; - - public KeyCannotSign() { - super(); - } - - public KeyCannotSign(String message) { - super(message); - } - - public KeyCannotSign(String s, Throwable throwable) { - super(s, throwable); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * User provided incompatible options (e.g. "--as=clearsigned --no-armor"). - */ - public static class IncompatibleOptions extends SOPGPException { - - public static final int EXIT_CODE = 83; - - public IncompatibleOptions() { - super(); - } - - public IncompatibleOptions(String errorMsg) { - super(errorMsg); - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } - - /** - * The user provided a subcommand with an unsupported profile ("--profile=XYZ"), - * or the user tried to list profiles of a subcommand that does not support profiles at all. - */ - public static class UnsupportedProfile extends SOPGPException { - - public static final int EXIT_CODE = 89; - - private final String subcommand; - private final String profile; - - /** - * Create an exception signalling a subcommand that does not support any profiles. - * - * @param subcommand subcommand - */ - public UnsupportedProfile(String subcommand) { - super("Subcommand '" + subcommand + "' does not support any profiles."); - this.subcommand = subcommand; - this.profile = null; - } - - /** - * Create an exception signalling a subcommand does not support a specific profile. - * - * @param subcommand subcommand - * @param profile unsupported profile - */ - public UnsupportedProfile(String subcommand, String profile) { - super("Subcommand '" + subcommand + "' does not support profile '" + profile + "'."); - this.subcommand = subcommand; - this.profile = profile; - } - - /** - * Wrap an exception into another instance with a possibly translated error message. - * - * @param errorMsg error message - * @param e exception - */ - public UnsupportedProfile(String errorMsg, UnsupportedProfile e) { - super(errorMsg, e); - this.subcommand = e.getSubcommand(); - this.profile = e.getProfile(); - } - - /** - * Return the subcommand name. - * - * @return subcommand - */ - public String getSubcommand() { - return subcommand; - } - - /** - * Return the profile name. - * May return
null
. - * - * @return profile name - */ - public String getProfile() { - return profile; - } - - @Override - public int getExitCode() { - return EXIT_CODE; - } - } -} diff --git a/sop-java/src/main/java/sop/exception/package-info.java b/sop-java/src/main/java/sop/exception/package-info.java deleted file mode 100644 index 4abc562..0000000 --- a/sop-java/src/main/java/sop/exception/package-info.java +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - * Exception classes. - */ -package sop.exception; diff --git a/sop-java/src/main/java/sop/package-info.java b/sop-java/src/main/java/sop/package-info.java deleted file mode 100644 index 5ad4f52..0000000 --- a/sop-java/src/main/java/sop/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Stateless OpenPGP Interface for Java. - */ -package sop; diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt new file mode 100644 index 0000000..2473258 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -0,0 +1,308 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.exception + +abstract class SOPGPException : RuntimeException { + + constructor() : super() + + constructor(message: String) : super(message) + + constructor(cause: Throwable) : super(cause) + + constructor(message: String, cause: Throwable) : super(message, cause) + + abstract fun getExitCode(): Int + + /** No acceptable signatures found (sop verify, inline-verify). */ + class NoSignature : SOPGPException { + @JvmOverloads + constructor(message: String = "No verifiable signature found.") : super(message) + + constructor(errorMsg: String, e: NoSignature) : super(errorMsg, e) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 3 + } + } + + /** Asymmetric algorithm unsupported (sop encrypt, sign, inline-sign). */ + class UnsupportedAsymmetricAlgo(message: String, e: Throwable) : SOPGPException(message, e) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 13 + } + } + + /** Certificate not encryption capable (e,g, expired, revoked, unacceptable usage). */ + class CertCannotEncrypt : SOPGPException { + constructor(message: String, cause: Throwable) : super(message, cause) + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 17 + } + } + + /** Missing required argument. */ + class MissingArg : SOPGPException { + constructor() + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 19 + } + } + + /** Incomplete verification instructions (sop decrypt). */ + class IncompleteVerification(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 23 + } + } + + /** Unable to decrypt (sop decrypt). */ + class CannotDecrypt : SOPGPException { + constructor() + + constructor(errorMsg: String, e: Throwable) : super(errorMsg, e) + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 29 + } + } + + /** Non-UTF-8 or otherwise unreliable password (sop encrypt). */ + class PasswordNotHumanReadable : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 31 + } + } + + /** Unsupported option. */ + class UnsupportedOption : SOPGPException { + constructor(message: String) : super(message) + + constructor(message: String, cause: Throwable) : super(message, cause) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 37 + } + } + + /** Invalid data type (no secret key where KEYS expected, etc.). */ + class BadData : SOPGPException { + constructor(message: String) : super(message) + + constructor(throwable: Throwable) : super(throwable) + + constructor(message: String, throwable: Throwable) : super(message, throwable) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 41 + } + } + + /** Non-Text input where text expected. */ + class ExpectedText : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 53 + } + } + + /** Output file already exists. */ + class OutputExists(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 59 + } + } + + /** Input file does not exist. */ + class MissingInput : SOPGPException { + constructor(message: String, cause: Throwable) : super(message, cause) + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 61 + } + } + + /** A KEYS input is protected (locked) with a password and sop failed to unlock it. */ + class KeyIsProtected : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + constructor(message: String, cause: Throwable) : super(message, cause) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 67 + } + } + + /** Unsupported subcommand. */ + class UnsupportedSubcommand(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 69 + } + } + + /** + * An indirect parameter is a special designator (it starts with @), but sop does not know how + * to handle the prefix. + */ + class UnsupportedSpecialPrefix(errorMsg: String) : SOPGPException(errorMsg) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 71 + } + } + + /** + * Exception that gets thrown if a special designator (starting with @) is given, but the + * filesystem contains a file matching the designator. + * + * E.g.
@ENV:FOO
is given, but
./@ENV:FOO
exists on the filesystem. + */ + class AmbiguousInput(message: String) : SOPGPException(message) { + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 73 + } + } + + /** Key not signature-capable (e.g. expired, revoked, unacceptable usage flags). */ + class KeyCannotSign : SOPGPException { + constructor() : super() + + constructor(message: String) : super(message) + + constructor(s: String, throwable: Throwable) : super(s, throwable) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 79 + } + } + + /** User provided incompatible options (e.g. "--as=clearsigned --no-armor"). */ + class IncompatibleOptions : SOPGPException { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 83 + } + } + + /** + * The user provided a subcommand with an unsupported profile ("--profile=XYZ"), or the user + * tried to list profiles of a subcommand that does not support profiles at all. + */ + class UnsupportedProfile : SOPGPException { + /** + * Return the subcommand name. + * + * @return subcommand + */ + val subcommand: String + + /** + * Return the profile name. May return `null`. + * + * @return profile name + */ + val profile: String? + + /** + * Create an exception signalling a subcommand that does not support any profiles. + * + * @param subcommand subcommand + */ + constructor( + subcommand: String + ) : super("Subcommand '$subcommand' does not support any profiles.") { + this.subcommand = subcommand + profile = null + } + + /** + * Create an exception signalling a subcommand does not support a specific profile. + * + * @param subcommand subcommand + * @param profile unsupported profile + */ + constructor( + subcommand: String, + profile: String + ) : super("Subcommand '$subcommand' does not support profile '$profile'.") { + this.subcommand = subcommand + this.profile = profile + } + + /** + * Wrap an exception into another instance with a possibly translated error message. + * + * @param errorMsg error message + * @param e exception + */ + constructor(errorMsg: String, e: UnsupportedProfile) : super(errorMsg, e) { + subcommand = e.subcommand + profile = e.profile + } + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 89 + } + } +} From 5ee9414410c76b02b20833c10a84f500dca582aa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:54:54 +0100 Subject: [PATCH 048/238] Encrypt: Add --session-key-out support --- .../main/java/sop/external/ExternalSOP.java | 2 +- .../external/operation/EncryptExternal.java | 62 +++++++++++++++++-- .../sop/cli/picocli/commands/EncryptCmd.java | 36 ++++++++++- .../cli/picocli/commands/EncryptCmdTest.java | 44 ++++++------- .../src/main/kotlin/sop/EncryptionResult.kt | 15 +++++ .../src/main/kotlin/sop/operation/Encrypt.kt | 13 ++-- .../operation/EncryptDecryptTest.java | 30 +++++++-- 7 files changed, 162 insertions(+), 40 deletions(-) create mode 100644 sop-java/src/main/kotlin/sop/EncryptionResult.kt diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index 376e54c..e1479ee 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -147,7 +147,7 @@ public class ExternalSOP implements SOP { @Override public Encrypt encrypt() { - return new EncryptExternal(binaryName, properties); + return new EncryptExternal(binaryName, properties, tempDirProvider); } @Override diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java index bc40208..3eabfc5 100644 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java @@ -4,14 +4,19 @@ package sop.external.operation; -import sop.Ready; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.SessionKey; import sop.enums.EncryptAs; import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Encrypt; +import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -21,6 +26,7 @@ import java.util.Properties; */ public class EncryptExternal implements Encrypt { + private final ExternalSOP.TempDirProvider tempDirProvider; private final List commandList = new ArrayList<>(); private final List envList; private int SIGN_WITH_COUNTER = 0; @@ -28,7 +34,8 @@ public class EncryptExternal implements Encrypt { private int PASSWORD_COUNTER = 0; private int CERT_COUNTER = 0; - public EncryptExternal(String binary, Properties environment) { + public EncryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { + this.tempDirProvider = tempDirProvider; this.commandList.add(binary); this.commandList.add("encrypt"); this.envList = ExternalSOP.propertiesToEnv(environment); @@ -92,8 +99,53 @@ public class EncryptExternal implements Encrypt { } @Override - public Ready plaintext(InputStream plaintext) - throws SOPGPException.KeyIsProtected { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext); + public ReadyWithResult plaintext(InputStream plaintext) + throws SOPGPException.KeyIsProtected, IOException { + File tempDir = tempDirProvider.provideTempDirectory(); + + File sessionKeyOut = new File(tempDir, "session-key-out"); + sessionKeyOut.delete(); + commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath()); + + String[] command = commandList.toArray(new String[0]); + String[] env = envList.toArray(new String[0]); + try { + Process process = Runtime.getRuntime().exec(command, env); + OutputStream processOut = process.getOutputStream(); + InputStream processIn = process.getInputStream(); + + return new ReadyWithResult() { + @Override + public EncryptionResult writeTo(OutputStream outputStream) throws IOException { + byte[] buf = new byte[4096]; + int r; + while ((r = plaintext.read(buf)) > 0) { + processOut.write(buf, 0, r); + } + + plaintext.close(); + processOut.close(); + + while ((r = processIn.read(buf)) > 0) { + outputStream.write(buf, 0 , r); + } + + processIn.close(); + outputStream.close(); + + ExternalSOP.finish(process); + + FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut); + String line = ExternalSOP.readString(sessionKeyOutIn); + SessionKey sessionKey = SessionKey.fromString(line.trim()); + sessionKeyOutIn.close(); + sessionKeyOut.delete(); + + return new EncryptionResult(sessionKey); + } + }; + } catch (IOException e) { + throw new RuntimeException(e); + } } } diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java index efda26f..0d37416 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java @@ -5,7 +5,9 @@ package sop.cli.picocli.commands; import picocli.CommandLine; -import sop.Ready; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.SessionKey; import sop.cli.picocli.SopCLI; import sop.enums.EncryptAs; import sop.exception.SOPGPException; @@ -13,6 +15,8 @@ import sop.operation.Encrypt; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; @@ -49,11 +53,18 @@ public class EncryptCmd extends AbstractSopCmd { paramLabel = "CERTS") List certs = new ArrayList<>(); + @CommandLine.Option( + names = {"--session-key-out"}, + paramLabel = "SESSIONKEY") + String sessionKeyOut; + @Override public void run() { Encrypt encrypt = throwIfUnsupportedSubcommand( SopCLI.getSop().encrypt(), "encrypt"); + throwIfOutputExists(sessionKeyOut); + if (profile != null) { try { encrypt.profile(profile); @@ -145,8 +156,27 @@ public class EncryptCmd extends AbstractSopCmd { } try { - Ready ready = encrypt.plaintext(System.in); - ready.writeTo(System.out); + ReadyWithResult ready = encrypt.plaintext(System.in); + EncryptionResult result = ready.writeTo(System.out); + + if (sessionKeyOut == null) { + return; + } + try (OutputStream outputStream = getOutput(sessionKeyOut)) { + if (!result.getSessionKey().isPresent()) { + String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted"); + throw new SOPGPException.UnsupportedOption(String.format(errorMsg, "--session-key-out")); + } + SessionKey sessionKey = result.getSessionKey().get(); + if (sessionKey == null) { + return; + } + PrintWriter writer = new PrintWriter(outputStream); + // CHECKSTYLE:OFF + writer.println(sessionKey); + // CHECKSTYLE:ON + writer.flush(); + } } catch (IOException e) { throw new RuntimeException(e); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java index 73ec9cb..09346af 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java @@ -4,6 +4,24 @@ package sop.cli.picocli.commands; +import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import sop.EncryptionResult; +import sop.ReadyWithResult; +import sop.SOP; +import sop.cli.picocli.SopCLI; +import sop.cli.picocli.TestFileUtil; +import sop.enums.EncryptAs; +import sop.exception.SOPGPException; +import sop.operation.Encrypt; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -11,22 +29,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import sop.Ready; -import sop.SOP; -import sop.cli.picocli.SopCLI; -import sop.cli.picocli.TestFileUtil; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - public class EncryptCmdTest { Encrypt encrypt; @@ -34,10 +36,10 @@ public class EncryptCmdTest { @BeforeEach public void mockComponents() throws IOException { encrypt = mock(Encrypt.class); - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { + when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) { - + public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { + return new EncryptionResult(null); } }); @@ -190,9 +192,9 @@ public class EncryptCmdTest { @Test @ExpectSystemExitWithStatus(1) public void writeTo_ioExceptionCausesExit1() throws IOException { - when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { + when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { throw new IOException(); } }); diff --git a/sop-java/src/main/kotlin/sop/EncryptionResult.kt b/sop-java/src/main/kotlin/sop/EncryptionResult.kt new file mode 100644 index 0000000..1284f89 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/EncryptionResult.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import sop.util.Optional + +class EncryptionResult(sessionKey: SessionKey?) { + val sessionKey: Optional + + init { + this.sessionKey = Optional.ofNullable(sessionKey) + } +} diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt index 0daebee..71c04cb 100644 --- a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -6,8 +6,9 @@ package sop.operation import java.io.IOException import java.io.InputStream +import sop.EncryptionResult import sop.Profile -import sop.Ready +import sop.ReadyWithResult import sop.enums.EncryptAs import sop.exception.SOPGPException.* import sop.util.UTF8Util @@ -146,20 +147,22 @@ interface Encrypt { * Encrypt the given data yielding the ciphertext. * * @param plaintext plaintext - * @return input stream containing the ciphertext + * @return result and ciphertext * @throws IOException in case of an IO error * @throws KeyIsProtected if at least one signing key cannot be unlocked */ - @Throws(IOException::class, KeyIsProtected::class) fun plaintext(plaintext: InputStream): Ready + @Throws(IOException::class, KeyIsProtected::class) + fun plaintext(plaintext: InputStream): ReadyWithResult /** * Encrypt the given data yielding the ciphertext. * * @param plaintext plaintext - * @return input stream containing the ciphertext + * @return result and ciphertext * @throws IOException in case of an IO error * @throws KeyIsProtected if at least one signing key cannot be unlocked */ @Throws(IOException::class, KeyIsProtected::class) - fun plaintext(plaintext: ByteArray): Ready = plaintext(plaintext.inputStream()) + fun plaintext(plaintext: ByteArray): ReadyWithResult = + plaintext(plaintext.inputStream()) } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java index 9138f64..51c117a 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -10,13 +10,16 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.ByteArrayAndResult; import sop.DecryptionResult; +import sop.EncryptionResult; import sop.SOP; +import sop.SessionKey; import sop.Verification; import sop.enums.EncryptAs; import sop.enums.SignatureMode; import sop.exception.SOPGPException; import sop.testsuite.TestData; import sop.testsuite.assertions.VerificationListAssert; +import sop.util.Optional; import sop.util.UTCUtil; import java.io.IOException; @@ -27,6 +30,7 @@ import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -41,18 +45,26 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + ByteArrayAndResult encResult = sop.encrypt() .withPassword("sw0rdf1sh") .plaintext(message) - .getBytes(); + .toByteArrayAndResult(); - byte[] plaintext = sop.decrypt() + byte[] ciphertext = encResult.getBytes(); + Optional encSessionKey = encResult.getResult().getSessionKey(); + + ByteArrayAndResult decResult = sop.decrypt() .withPassword("sw0rdf1sh") .ciphertext(ciphertext) - .toByteArrayAndResult() - .getBytes(); + .toByteArrayAndResult(); + + byte[] plaintext = decResult.getBytes(); + Optional decSessionKey = decResult.getResult().getSessionKey(); assertArrayEquals(message, plaintext); + if (encSessionKey.isPresent() && decSessionKey.isPresent()) { + assertEquals(encSessionKey.get(), decSessionKey.get()); + } } @ParameterizedTest @@ -62,6 +74,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -83,6 +96,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) + .toByteArrayAndResult() .getBytes(); byte[] plaintext = sop.decrypt() @@ -101,6 +115,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = sop.encrypt() .withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) + .toByteArrayAndResult() .getBytes(); byte[] plaintext = sop.decrypt() @@ -120,6 +135,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .noArmor() .plaintext(message) + .toByteArrayAndResult() .getBytes(); byte[] armored = sop.armor() @@ -144,6 +160,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.binary) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -175,6 +192,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.text) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -215,6 +233,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .signWith(key) .withKeyPassword(keyPassword) .plaintext(message) + .toByteArrayAndResult() .getBytes(); ByteArrayAndResult bytesAndResult = sop.decrypt() @@ -305,6 +324,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt() .plaintext(message) + .toByteArrayAndResult() .getBytes()); } } From 86b173bf1c4b5daa4b58abf2390597b7eee0d73b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 16:16:32 +0100 Subject: [PATCH 049/238] Update Changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ca12a2..d736ded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 8.0.0-SNAPSHOT +- Rewrote API in Kotlin +- Update implementation to [SOP Specification revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html). + - Add `--no-armor` option to `revoke-key` and `change-key-password` subcommands + - `armor`: Deprecate `--label` option + - `encrypt`: Add `--session-key-out` option +- Slight API changes: + - `sop.encrypt().plaintext()` now returns a `ReadyWithResult` instead of `Ready`. + - `EncryptionResult` is a new result type, that provides access to the session key of an encrypted message + - Change `ArmorLabel` values into lowercase + - Change `EncryptAs` values into lowercase + - Change `SignAs` values into lowercase + ## 7.0.0 - Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html). - Add support for new `revoke-key` subcommand From 1de179c0156c7bad489f05bd4c0a39cf06f96286 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 16:07:37 +0100 Subject: [PATCH 050/238] Kotlin conversion: VersionCmd --- .../sop/cli/picocli/commands/VersionCmd.java | 58 ------------------- .../sop/cli/picocli/commands/VersionCmd.kt | 51 ++++++++++++++++ 2 files changed, 51 insertions(+), 58 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java deleted file mode 100644 index 6ccb8f7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VersionCmd.java +++ /dev/null @@ -1,58 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Version; - -@CommandLine.Command(name = "version", resourceBundle = "msg_version", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class VersionCmd extends AbstractSopCmd { - - @CommandLine.ArgGroup() - Exclusive exclusive; - - static class Exclusive { - @CommandLine.Option(names = "--extended") - boolean extended; - - @CommandLine.Option(names = "--backend") - boolean backend; - - @CommandLine.Option(names = "--sop-spec") - boolean sopSpec; - } - - - - @Override - public void run() { - Version version = throwIfUnsupportedSubcommand( - SopCLI.getSop().version(), "version"); - - if (exclusive == null) { - Print.outln(version.getName() + " " + version.getVersion()); - return; - } - - if (exclusive.extended) { - Print.outln(version.getExtendedVersion()); - return; - } - - if (exclusive.backend) { - Print.outln(version.getBackendVersion()); - return; - } - - if (exclusive.sopSpec) { - Print.outln(version.getSopSpecVersion()); - return; - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt new file mode 100644 index 0000000..75197fe --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.ArgGroup +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "version", + resourceBundle = "msg_version", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class VersionCmd : AbstractSopCmd() { + + @ArgGroup var exclusive: Exclusive? = null + + class Exclusive { + @Option(names = ["--extended"]) var extended: Boolean = false + @Option(names = ["--backend"]) var backend: Boolean = false + @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false + } + + override fun run() { + val version = throwIfUnsupportedSubcommand(SopCLI.getSop().version(), "version") + + if (exclusive == null) { + // No option provided + println("${version.getName()} ${version.getVersion()}") + return + } + + if (exclusive!!.extended) { + println(version.getExtendedVersion()) + return + } + + if (exclusive!!.backend) { + println(version.getBackendVersion()) + return + } + + if (exclusive!!.sopSpec) { + println(version.getSopSpecVersion()) + return + } + } +} From 8246359a854ec3d2fe715ccf683e908afb981c68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 16:15:35 +0100 Subject: [PATCH 051/238] Kotlin conversion: VerifyCmd --- .../sop/cli/picocli/commands/VerifyCmd.java | 102 ------------------ .../sop/cli/picocli/commands/VerifyCmd.kt | 81 ++++++++++++++ 2 files changed, 81 insertions(+), 102 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java deleted file mode 100644 index d76bb37..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/VerifyCmd.java +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Verification; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.DetachedVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "verify", - resourceBundle = "msg_detached-verify", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class VerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(index = "0", - paramLabel = "SIGNATURE") - String signature; - - @CommandLine.Parameters(index = "1..*", - arity = "1..*", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - paramLabel = "DATE") - String notAfter = "now"; - - @Override - public void run() { - DetachedVerify detachedVerify = throwIfUnsupportedSubcommand( - SopCLI.getSop().detachedVerify(), "verify"); - - if (notAfter != null) { - try { - detachedVerify.notAfter(parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - if (notBefore != null) { - try { - detachedVerify.notBefore(parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String certInput : certificates) { - try (InputStream certIn = getInput(certInput)) { - detachedVerify.cert(certIn); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (signature != null) { - try (InputStream sigIn = getInput(signature)) { - detachedVerify.signatures(sigIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_signature", signature); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - List verifications; - try { - verifications = detachedVerify.data(System.in); - } catch (SOPGPException.NoSignature e) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } - - for (Verification verification : verifications) { - Print.outln(verification.toString()); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt new file mode 100644 index 0000000..ef27266 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VerifyCmd.kt @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "verify", + resourceBundle = "msg_detached-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class VerifyCmd : AbstractSopCmd() { + + @Parameters(index = "0", paramLabel = "SIGNATURE") lateinit var signature: String + + @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERT") + lateinit var certificates: List + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + override fun run() { + val detachedVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().detachedVerify(), "verify") + try { + detachedVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + detachedVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> detachedVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + try { + getInput(signature).use { sigIn -> detachedVerify.signatures(sigIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_signature", signature) + throw BadData(errorMsg, badData) + } + + val verifications = + try { + detachedVerify.data(System.`in`) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + for (verification in verifications) { + println(verification.toString()) + } + } +} From 256d1c596067fa91ff0d542981a2ca8f9141deae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 16:34:35 +0100 Subject: [PATCH 052/238] Kotlin conversion: SignCmd --- .../sop/cli/picocli/commands/SignCmd.java | 108 ------------------ .../sop/cli/picocli/commands/SignCmd.kt | 90 +++++++++++++++ 2 files changed, 90 insertions(+), 108 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java deleted file mode 100644 index cad9d6e..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/SignCmd.java +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.cli.picocli.SopCLI; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.operation.DetachedSign; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "sign", - resourceBundle = "msg_detached-sign", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class SignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - paramLabel = "{binary|text}") - SignAs type; - - @CommandLine.Parameters(paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--micalg-out", - paramLabel = "MICALG") - String micAlgOut; - - @Override - public void run() { - DetachedSign detachedSign = throwIfUnsupportedSubcommand( - SopCLI.getSop().detachedSign(), "sign"); - - throwIfOutputExists(micAlgOut); - throwIfEmptyParameters(secretKeyFile, "KEYS"); - - if (type != null) { - try { - detachedSign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - detachedSign.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : secretKeyFile) { - try (InputStream keyIn = getInput(keyInput)) { - detachedSign.key(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - detachedSign.noArmor(); - } - - try { - ReadyWithResult ready = detachedSign.data(System.in); - SigningResult result = ready.writeTo(System.out); - - MicAlg micAlg = result.getMicAlg(); - if (micAlgOut != null) { - // Write micalg out - OutputStream outputStream = getOutput(micAlgOut); - micAlg.writeTo(outputStream); - outputStream.close(); - } - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt new file mode 100644 index 0000000..6860477 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/SignCmd.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.SignAs +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "sign", + resourceBundle = "msg_detached-sign", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class SignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + @Option(names = ["--as"], paramLabel = "{binary|text}") var type: SignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--micalg-out"], paramLabel = "MICALG") var micAlgOut: String? = null + + override fun run() { + val detachedSign = throwIfUnsupportedSubcommand(SopCLI.getSop().detachedSign(), "sign") + + throwIfOutputExists(micAlgOut) + throwIfEmptyParameters(secretKeyFile, "KEYS") + + try { + type?.let { detachedSign.mode(it) } + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } + + withKeyPassword.forEach { passIn -> + try { + val password = stringFromInputStream(getInput(passIn)) + detachedSign.withKeyPassword(password) + } catch (unsupported: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, unsupported) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + secretKeyFile.forEach { keyIn -> + try { + getInput(keyIn).use { input -> detachedSign.key(input) } + } catch (ioe: IOException) { + throw RuntimeException(ioe) + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyIn) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyIn) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + detachedSign.noArmor() + } + + try { + val ready = detachedSign.data(System.`in`) + val result = ready.writeTo(System.out) + + if (micAlgOut != null) { + getOutput(micAlgOut).use { result.micAlg.writeTo(it) } + } + } catch (e: IOException) { + throw java.lang.RuntimeException(e) + } + } +} From 666d51384b442ffa8621299b7874a1fabba6c020 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:20:00 +0100 Subject: [PATCH 053/238] Kotlin conversion: AbstractSopCmd --- .../cli/picocli/commands/AbstractSopCmd.java | 282 ------------------ .../cli/picocli/commands/AbstractSopCmd.kt | 248 +++++++++++++++ .../picocli/commands/AbstractSopCmdTest.java | 2 +- 3 files changed, 249 insertions(+), 283 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java deleted file mode 100644 index 9aec5a7..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/AbstractSopCmd.java +++ /dev/null @@ -1,282 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import sop.exception.SOPGPException; -import sop.util.UTCUtil; -import sop.util.UTF8Util; - -import javax.annotation.Nonnull; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.text.ParseException; -import java.util.Collection; -import java.util.Date; -import java.util.Locale; -import java.util.ResourceBundle; -import java.util.regex.Pattern; - -/** - * Abstract super class of SOP subcommands. - */ -public abstract class AbstractSopCmd implements Runnable { - - /** - * Interface to modularize resolving of environment variables. - */ - public interface EnvironmentVariableResolver { - /** - * Resolve the value of the given environment variable. - * Return null if the variable is not present. - * - * @param name name of the variable - * @return variable value or null - */ - String resolveEnvironmentVariable(String name); - } - - public static final String PRFX_ENV = "@ENV:"; - public static final String PRFX_FD = "@FD:"; - public static final Date BEGINNING_OF_TIME = new Date(0); - public static final Date END_OF_TIME = new Date(8640000000000000L); - - public static final Pattern PATTERN_FD = Pattern.compile("^\\d{1,20}$"); - - protected final ResourceBundle messages; - protected EnvironmentVariableResolver envResolver = System::getenv; - - public AbstractSopCmd() { - this(Locale.getDefault()); - } - - public AbstractSopCmd(@Nonnull Locale locale) { - messages = ResourceBundle.getBundle("msg_sop", locale); - } - - void throwIfOutputExists(String output) { - if (output == null) { - return; - } - - File outputFile = new File(output); - if (outputFile.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", outputFile.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - } - - public String getMsg(String key) { - return messages.getString(key); - } - - public String getMsg(String key, String arg1) { - return String.format(messages.getString(key), arg1); - } - - public String getMsg(String key, String arg1, String arg2) { - return String.format(messages.getString(key), arg1, arg2); - } - - void throwIfMissingArg(Object arg, String argName) { - if (arg == null) { - String errorMsg = getMsg("sop.error.usage.argument_required", argName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - void throwIfEmptyParameters(Collection arg, String parmName) { - if (arg.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.parameter_required", parmName); - throw new SOPGPException.MissingArg(errorMsg); - } - } - - T throwIfUnsupportedSubcommand(T subcommand, String subcommandName) { - if (subcommand == null) { - String errorMsg = getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName); - throw new SOPGPException.UnsupportedSubcommand(errorMsg); - } - return subcommand; - } - - void setEnvironmentVariableResolver(EnvironmentVariableResolver envResolver) { - if (envResolver == null) { - throw new NullPointerException("Variable envResolver cannot be null."); - } - this.envResolver = envResolver; - } - - public InputStream getInput(String indirectInput) throws IOException { - if (indirectInput == null) { - throw new IllegalArgumentException("Input cannot not be null."); - } - - String trimmed = indirectInput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Input cannot be blank."); - } - - if (trimmed.startsWith(PRFX_ENV)) { - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - String envName = trimmed.substring(PRFX_ENV.length()); - String envValue = envResolver.resolveEnvironmentVariable(envName); - if (envValue == null) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName); - throw new IllegalArgumentException(errorMsg); - } - - if (envValue.trim().isEmpty()) { - String errorMsg = getMsg("sop.error.indirect_data_type.environment_variable_empty", envName); - throw new IllegalArgumentException(errorMsg); - } - - return new ByteArrayInputStream(envValue.getBytes("UTF8")); - - } else if (trimmed.startsWith(PRFX_FD)) { - - if (new File(trimmed).exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed); - throw new SOPGPException.AmbiguousInput(errorMsg); - } - - File fdFile = fileDescriptorFromString(trimmed); - try { - FileInputStream fileIn = new FileInputStream(fdFile); - return fileIn; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } else { - File file = new File(trimmed); - if (!file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_file_does_not_exist", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - if (!file.isFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.input_not_a_file", file.getAbsolutePath()); - throw new SOPGPException.MissingInput(errorMsg); - } - - return new FileInputStream(file); - } - } - - public OutputStream getOutput(String indirectOutput) throws IOException { - if (indirectOutput == null) { - throw new IllegalArgumentException("Output cannot be null."); - } - - String trimmed = indirectOutput.trim(); - if (trimmed.isEmpty()) { - throw new IllegalArgumentException("Output cannot be blank."); - } - - // @ENV not allowed for output - if (trimmed.startsWith(PRFX_ENV)) { - String errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - - // File Descriptor - if (trimmed.startsWith(PRFX_FD)) { - File fdFile = fileDescriptorFromString(trimmed); - try { - FileOutputStream fout = new FileOutputStream(fdFile); - return fout; - } catch (FileNotFoundException e) { - String errorMsg = getMsg("sop.error.indirect_data_type.file_descriptor_not_found", fdFile.getAbsolutePath()); - throw new IOException(errorMsg, e); - } - } - - File file = new File(trimmed); - if (file.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_already_exists", file.getAbsolutePath()); - throw new SOPGPException.OutputExists(errorMsg); - } - - if (!file.createNewFile()) { - String errorMsg = getMsg("sop.error.indirect_data_type.output_file_cannot_be_created", file.getAbsolutePath()); - throw new IOException(errorMsg); - } - - return new FileOutputStream(file); - } - - public File fileDescriptorFromString(String fdString) { - File fdDir = new File("/dev/fd/"); - if (!fdDir.exists()) { - String errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported"); - throw new SOPGPException.UnsupportedSpecialPrefix(errorMsg); - } - String fdNumber = fdString.substring(PRFX_FD.length()); - if (!PATTERN_FD.matcher(fdNumber).matches()) { - throw new IllegalArgumentException("File descriptor must be a positive number."); - } - File descriptor = new File(fdDir, fdNumber); - return descriptor; - } - - public static String stringFromInputStream(InputStream inputStream) throws IOException { - try { - ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; int read; - while ((read = inputStream.read(buf)) != -1) { - byteOut.write(buf, 0, read); - } - // TODO: For decrypt operations we MUST accept non-UTF8 passwords - return UTF8Util.decodeUTF8(byteOut.toByteArray()); - } finally { - inputStream.close(); - } - } - - public Date parseNotAfter(String notAfter) { - if (notAfter.equals("now")) { - return new Date(); - } - - if (notAfter.equals("-")) { - return END_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notAfter); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_after"); - throw new IllegalArgumentException(errorMsg); - } - } - - public Date parseNotBefore(String notBefore) { - if (notBefore.equals("now")) { - return new Date(); - } - - if (notBefore.equals("-")) { - return BEGINNING_OF_TIME; - } - - try { - return UTCUtil.parseUTCDate(notBefore); - } catch (ParseException e) { - String errorMsg = getMsg("sop.error.input.malformed_not_before"); - throw new IllegalArgumentException(errorMsg); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt new file mode 100644 index 0000000..4629e57 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.* +import java.text.ParseException +import java.util.* +import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver +import sop.exception.SOPGPException.* +import sop.util.UTCUtil.Companion.parseUTCDate +import sop.util.UTF8Util.Companion.decodeUTF8 + +/** Abstract super class of SOP subcommands. */ +abstract class AbstractSopCmd(locale: Locale = Locale.getDefault()) : Runnable { + + private val messages: ResourceBundle = ResourceBundle.getBundle("msg_sop", locale) + var environmentVariableResolver = EnvironmentVariableResolver { name: String -> + System.getenv(name) + } + + /** Interface to modularize resolving of environment variables. */ + fun interface EnvironmentVariableResolver { + + /** + * Resolve the value of the given environment variable. Return null if the variable is not + * present. + * + * @param name name of the variable + * @return variable value or null + */ + fun resolveEnvironmentVariable(name: String): String? + } + + fun throwIfOutputExists(output: String?) { + output + ?.let { File(it) } + ?.let { + if (it.exists()) { + val errorMsg: String = + getMsg( + "sop.error.indirect_data_type.output_file_already_exists", + it.absolutePath) + throw OutputExists(errorMsg) + } + } + } + + fun getMsg(key: String): String = messages.getString(key) + + fun getMsg(key: String, vararg args: String): String { + val msg = messages.getString(key) + return String.format(msg, *args) + } + + fun throwIfMissingArg(arg: Any?, argName: String) { + if (arg == null) { + val errorMsg = getMsg("sop.error.usage.argument_required", argName) + throw MissingArg(errorMsg) + } + } + + fun throwIfEmptyParameters(arg: Collection<*>, parmName: String) { + if (arg.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", parmName) + throw MissingArg(errorMsg) + } + } + + fun throwIfUnsupportedSubcommand(subcommand: T?, subcommandName: String): T { + if (subcommand == null) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_not_supported", subcommandName) + throw UnsupportedSubcommand(errorMsg) + } + return subcommand + } + + @Throws(IOException::class) + fun getInput(indirectInput: String): InputStream { + val trimmed = indirectInput.trim() + require(trimmed.isNotBlank()) { "Input cannot be blank." } + + if (trimmed.startsWith(PRFX_ENV)) { + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val envName = trimmed.substring(PRFX_ENV.length) + val envValue = environmentVariableResolver.resolveEnvironmentVariable(envName) + requireNotNull(envValue) { + getMsg("sop.error.indirect_data_type.environment_variable_not_set", envName) + } + + require(envValue.trim().isNotEmpty()) { + getMsg("sop.error.indirect_data_type.environment_variable_empty", envName) + } + + return envValue.byteInputStream() + } else if (trimmed.startsWith(PRFX_FD)) { + + if (File(trimmed).exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.ambiguous_filename", trimmed) + throw AmbiguousInput(errorMsg) + } + + val fdFile: File = fileDescriptorFromString(trimmed) + return try { + fdFile.inputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } else { + + val file = File(trimmed) + if (!file.exists()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.input_file_does_not_exist", file.absolutePath) + throw MissingInput(errorMsg) + } + if (!file.isFile()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.input_not_a_file", file.absolutePath) + throw MissingInput(errorMsg) + } + return file.inputStream() + } + } + + @Throws(IOException::class) + fun getOutput(indirectOutput: String?): OutputStream { + requireNotNull(indirectOutput) { "Output cannot be null." } + val trimmed = indirectOutput.trim() + require(trimmed.isNotEmpty()) { "Output cannot be blank." } + + // @ENV not allowed for output + if (trimmed.startsWith(PRFX_ENV)) { + val errorMsg = getMsg("sop.error.indirect_data_type.illegal_use_of_env_designator") + throw UnsupportedSpecialPrefix(errorMsg) + } + + // File Descriptor + if (trimmed.startsWith(PRFX_FD)) { + val fdFile = fileDescriptorFromString(trimmed) + return try { + fdFile.outputStream() + } catch (e: FileNotFoundException) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.file_descriptor_not_found", + fdFile.absolutePath) + throw IOException(errorMsg, e) + } + } + val file = File(trimmed) + if (file.exists()) { + val errorMsg = + getMsg("sop.error.indirect_data_type.output_file_already_exists", file.absolutePath) + throw OutputExists(errorMsg) + } + if (!file.createNewFile()) { + val errorMsg = + getMsg( + "sop.error.indirect_data_type.output_file_cannot_be_created", file.absolutePath) + throw IOException(errorMsg) + } + return file.outputStream() + } + + fun fileDescriptorFromString(fdString: String): File { + val fdDir = File("/dev/fd/") + if (!fdDir.exists()) { + val errorMsg = getMsg("sop.error.indirect_data_type.designator_fd_not_supported") + throw UnsupportedSpecialPrefix(errorMsg) + } + val fdNumber = fdString.substring(PRFX_FD.length) + require(PATTERN_FD.matcher(fdNumber).matches()) { + "File descriptor must be a positive number." + } + return File(fdDir, fdNumber) + } + + fun parseNotAfter(notAfter: String): Date { + return when (notAfter) { + "now" -> Date() + "-" -> END_OF_TIME + else -> + try { + parseUTCDate(notAfter) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_after") + throw IllegalArgumentException(errorMsg) + } + } + } + + fun parseNotBefore(notBefore: String): Date { + return when (notBefore) { + "now" -> Date() + "-" -> DAWN_OF_TIME + else -> + try { + parseUTCDate(notBefore) + } catch (e: ParseException) { + val errorMsg = getMsg("sop.error.input.malformed_not_before") + throw IllegalArgumentException(errorMsg) + } + } + } + + companion object { + const val PRFX_ENV = "@ENV:" + + const val PRFX_FD = "@FD:" + + @JvmField val DAWN_OF_TIME = Date(0) + + @JvmField + @Deprecated("Replace with DAWN_OF_TIME", ReplaceWith("DAWN_OF_TIME")) + val BEGINNING_OF_TIME = DAWN_OF_TIME + + @JvmField val END_OF_TIME = Date(8640000000000000L) + + @JvmField val PATTERN_FD = "^\\d{1,20}$".toPattern() + + @Throws(IOException::class) + @JvmStatic + fun stringFromInputStream(inputStream: InputStream): String { + return inputStream.use { input -> + val byteOut = ByteArrayOutputStream() + val buf = ByteArray(4096) + var read: Int + while (input.read(buf).also { read = it } != -1) { + byteOut.write(buf, 0, read) + } + // TODO: For decrypt operations we MUST accept non-UTF8 passwords + decodeUTF8(byteOut.toByteArray()) + } + } + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java index aed420b..396bc7f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/AbstractSopCmdTest.java @@ -36,7 +36,7 @@ public class AbstractSopCmdTest { @Test public void getInput_NullInvalid() { - assertThrows(IllegalArgumentException.class, () -> abstractCmd.getInput(null)); + assertThrows(NullPointerException.class, () -> abstractCmd.getInput(null)); } @Test From 18865feaff9192ac63d6f6a44b1ec6f3648621ce Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:26:38 +0100 Subject: [PATCH 054/238] Kotlin conversion: RevokeKeyCmd --- .../cli/picocli/commands/RevokeKeyCmd.java | 62 ------------------- .../sop/cli/picocli/commands/RevokeKeyCmd.kt | 58 +++++++++++++++++ 2 files changed, 58 insertions(+), 62 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java deleted file mode 100644 index 45f22fa..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/RevokeKeyCmd.java +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.RevokeKey; - -import java.io.IOException; - -@CommandLine.Command(name = "revoke-key", - resourceBundle = "msg_revoke-key", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class RevokeKeyCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - String withKeyPassword; - - @Override - public void run() { - RevokeKey revokeKey = throwIfUnsupportedSubcommand( - SopCLI.getSop().revokeKey(), "revoke-key"); - - if (!armor) { - revokeKey.noArmor(); - } - - if (withKeyPassword != null) { - try { - String password = stringFromInputStream(getInput(withKeyPassword)); - revokeKey.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption e) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - Ready ready; - try { - ready = revokeKey.keys(System.in); - } catch (SOPGPException.KeyIsProtected e) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN"); - throw new SOPGPException.KeyIsProtected(errorMsg, e); - } - try { - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt new file mode 100644 index 0000000..0b93ac5 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.KeyIsProtected + +@Command( + name = "revoke-key", + resourceBundle = "msg_revoke-key", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class RevokeKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: String? = null + + override fun run() { + val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") + + if (!armor) { + revokeKey.noArmor() + } + + withKeyPassword?.let { + try { + val password = stringFromInputStream(getInput(it)) + revokeKey.withKeyPassword(password) + } catch (e: SOPGPException.UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw SOPGPException.UnsupportedOption(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + val ready = + try { + revokeKey.keys(System.`in`) + } catch (e: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", "STANDARD_IN") + throw KeyIsProtected(errorMsg, e) + } + try { + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 377a7287b3a8fc2e26eba4608964844eac63b2de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:34:30 +0100 Subject: [PATCH 055/238] Kotlin conversion: ArmorCmd --- .../sop/cli/picocli/commands/ArmorCmd.java | 50 ------------------- .../sop/cli/picocli/commands/ArmorCmd.kt | 42 ++++++++++++++++ 2 files changed, 42 insertions(+), 50 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java deleted file mode 100644 index 5691686..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.operation.Armor; - -import java.io.IOException; - -@CommandLine.Command(name = "armor", - resourceBundle = "msg_armor", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ArmorCmd extends AbstractSopCmd { - - @CommandLine.Option(names = {"--label"}, - paramLabel = "{auto|sig|key|cert|message}") - ArmorLabel label; - - @Override - public void run() { - Armor armor = throwIfUnsupportedSubcommand( - SopCLI.getSop().armor(), - "armor"); - - if (label != null) { - try { - armor.label(label); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - try { - Ready ready = armor.data(System.in); - ready.writeTo(System.out); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt new file mode 100644 index 0000000..cb6bc79 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -0,0 +1,42 @@ +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.enums.ArmorLabel +import sop.exception.SOPGPException.BadData +import sop.exception.SOPGPException.UnsupportedOption + +@Command( + name = "armor", + resourceBundle = "msg_armor", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class ArmorCmd : AbstractSopCmd() { + + @Option(names = ["--label"], paramLabel = "{auto|sig|key|cert|message}") + var label: ArmorLabel? = null + + override fun run() { + val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor") + + label?.let { + try { + armor.label(it) + } catch (unsupported: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label") + throw UnsupportedOption(errorMsg, unsupported) + } + } + + try { + val ready = armor.data(System.`in`) + ready.writeTo(System.out) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + throw BadData(errorMsg, badData) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 49120c5da883dca98ec243577b9368c9569dd858 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:41:07 +0100 Subject: [PATCH 056/238] Kotlin conversion: ChangeKeyPasswordCmd --- .../commands/ChangeKeyPasswordCmd.java | 56 ------------------- .../picocli/commands/ChangeKeyPasswordCmd.kt | 46 +++++++++++++++ 2 files changed, 46 insertions(+), 56 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java deleted file mode 100644 index 5a6aa2a..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ChangeKeyPasswordCmd.java +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ChangeKeyPassword; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "change-key-password", - resourceBundle = "msg_change-key-password", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ChangeKeyPasswordCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--old-key-password"}, - paramLabel = "PASSWORD") - List oldKeyPasswords = new ArrayList<>(); - - @CommandLine.Option(names = {"--new-key-password"}, arity = "0..1", - paramLabel = "PASSWORD") - String newKeyPassword = null; - - @Override - public void run() { - ChangeKeyPassword changeKeyPassword = throwIfUnsupportedSubcommand( - SopCLI.getSop().changeKeyPassword(), "change-key-password"); - - if (!armor) { - changeKeyPassword.noArmor(); - } - - for (String oldKeyPassword : oldKeyPasswords) { - changeKeyPassword.oldKeyPassphrase(oldKeyPassword); - } - - if (newKeyPassword != null) { - changeKeyPassword.newKeyPassphrase(newKeyPassword); - } - - try { - changeKeyPassword.keys(System.in).writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt new file mode 100644 index 0000000..0c2eb4a --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.lang.RuntimeException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "change-key-password", + resourceBundle = "msg_change-key-password", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ChangeKeyPasswordCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + @Option(names = ["--old-key-password"], paramLabel = "PASSWORD") + var oldKeyPasswords: List = listOf() + + @Option(names = ["--new-key-password"], arity = "0..1", paramLabel = "PASSWORD") + var newKeyPassword: String? = null + + override fun run() { + val changeKeyPassword = + throwIfUnsupportedSubcommand(SopCLI.getSop().changeKeyPassword(), "change-key-password") + + if (!armor) { + changeKeyPassword.noArmor() + } + + oldKeyPasswords.forEach { changeKeyPassword.oldKeyPassphrase(it) } + + newKeyPassword?.let { changeKeyPassword.newKeyPassphrase(it) } + + try { + changeKeyPassword.keys(System.`in`).writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 688b8043a26cce2a80807db136ababd656cb978a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:45:56 +0100 Subject: [PATCH 057/238] Kotlin conversion: DearmorCmd --- .../sop/cli/picocli/commands/DearmorCmd.java | 47 ------------------- .../sop/cli/picocli/commands/DearmorCmd.kt | 37 +++++++++++++++ 2 files changed, 37 insertions(+), 47 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java deleted file mode 100644 index f73e351..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DearmorCmd.java +++ /dev/null @@ -1,47 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Dearmor; - -import java.io.IOException; - -@CommandLine.Command(name = "dearmor", - resourceBundle = "msg_dearmor", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DearmorCmd extends AbstractSopCmd { - - @Override - public void run() { - Dearmor dearmor = throwIfUnsupportedSubcommand( - SopCLI.getSop().dearmor(), "dearmor"); - - try { - dearmor.data(System.in) - .writeTo(System.out); - } catch (SOPGPException.BadData e) { - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - throw new SOPGPException.BadData(errorMsg, e); - } catch (IOException e) { - String msg = e.getMessage(); - if (msg == null) { - throw new RuntimeException(e); - } - - String errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data"); - if (msg.equals("invalid armor") || - msg.equals("invalid armor header") || - msg.equals("inconsistent line endings in headers") || - msg.startsWith("unable to decode base64 data")) { - throw new SOPGPException.BadData(errorMsg, e); - } - - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt new file mode 100644 index 0000000..9ae97c6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt @@ -0,0 +1,37 @@ +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData + +@Command( + name = "dearmor", + resourceBundle = "msg_dearmor", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class DearmorCmd : AbstractSopCmd() { + + override fun run() { + val dearmor = throwIfUnsupportedSubcommand(SopCLI.getSop().dearmor(), "dearmor") + + try { + dearmor.data(System.`in`).writeTo(System.out) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + throw BadData(errorMsg, badData) + } catch (e: IOException) { + e.message?.let { + val errorMsg = getMsg("sop.error.input.stdin_not_openpgp_data") + if (it == "invalid armor" || + it == "invalid armor header" || + it == "inconsistent line endings in headers" || + it.startsWith("unable to decode base64 data")) { + throw BadData(errorMsg, e) + } + throw RuntimeException(e) + } + ?: throw RuntimeException(e) + } + } +} From 8e65771e3638ac57c7ef3befed334fbf48c0c3e3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 17:50:07 +0100 Subject: [PATCH 058/238] Kotlin conversion: ListProfilesCmd --- .../cli/picocli/commands/ListProfilesCmd.java | 36 ------------------- .../cli/picocli/commands/ListProfilesCmd.kt | 34 ++++++++++++++++++ 2 files changed, 34 insertions(+), 36 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java deleted file mode 100644 index 53ec024..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ListProfilesCmd.java +++ /dev/null @@ -1,36 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Profile; -import sop.cli.picocli.Print; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ListProfiles; - -@CommandLine.Command(name = "list-profiles", - resourceBundle = "msg_list-profiles", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ListProfilesCmd extends AbstractSopCmd { - - @CommandLine.Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") - String subcommand; - - @Override - public void run() { - ListProfiles listProfiles = throwIfUnsupportedSubcommand( - SopCLI.getSop().listProfiles(), "list-profiles"); - - try { - for (Profile profile : listProfiles.subcommand(subcommand)) { - Print.outln(profile.toString()); - } - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt new file mode 100644 index 0000000..b770e82 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ListProfilesCmd.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.Command +import picocli.CommandLine.Parameters +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "list-profiles", + resourceBundle = "msg_list-profiles", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ListProfilesCmd : AbstractSopCmd() { + + @Parameters(paramLabel = "COMMAND", arity = "1", descriptionKey = "subcommand") + lateinit var subcommand: String + + override fun run() { + val listProfiles = + throwIfUnsupportedSubcommand(SopCLI.getSop().listProfiles(), "list-profiles") + + try { + listProfiles.subcommand(subcommand).forEach { println(it) } + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.feature_support.subcommand_does_not_support_profiles", subcommand) + throw UnsupportedProfile(errorMsg, e) + } + } +} From 9daabb758a559b48b24582cd5f1e776c29c29309 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 18:18:55 +0100 Subject: [PATCH 059/238] Kotlin conversion: DecryptCmd --- .../sop/cli/picocli/commands/DecryptCmd.java | 255 ------------------ .../sop/cli/picocli/commands/DecryptCmd.kt | 223 +++++++++++++++ .../cli/picocli/commands/DecryptCmdTest.java | 6 +- 3 files changed, 226 insertions(+), 258 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java deleted file mode 100644 index a681b4d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ /dev/null @@ -1,255 +0,0 @@ -// SPDX-FileCopyrightText: 2020 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.Decrypt; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - -@CommandLine.Command(name = "decrypt", - resourceBundle = "msg_decrypt", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class DecryptCmd extends AbstractSopCmd { - - private static final String OPT_SESSION_KEY_OUT = "--session-key-out"; - private static final String OPT_WITH_SESSION_KEY = "--with-session-key"; - private static final String OPT_WITH_PASSWORD = "--with-password"; - private static final String OPT_WITH_KEY_PASSWORD = "--with-key-password"; - private static final String OPT_VERIFICATIONS_OUT = "--verifications-out"; // see SOP-05 - private static final String OPT_VERIFY_WITH = "--verify-with"; - private static final String OPT_NOT_BEFORE = "--verify-not-before"; - private static final String OPT_NOT_AFTER = "--verify-not-after"; - - - @CommandLine.Option( - names = {OPT_SESSION_KEY_OUT}, - paramLabel = "SESSIONKEY") - String sessionKeyOut; - - @CommandLine.Option( - names = {OPT_WITH_SESSION_KEY}, - paramLabel = "SESSIONKEY") - List withSessionKey = new ArrayList<>(); - - @CommandLine.Option( - names = {OPT_WITH_PASSWORD}, - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_VERIFICATIONS_OUT, "--verify-out"}, // TODO: Remove --verify-out in 06 - paramLabel = "VERIFICATIONS") - String verifyOut; - - @CommandLine.Option(names = {OPT_VERIFY_WITH}, - paramLabel = "CERT") - List certs = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_NOT_BEFORE}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {OPT_NOT_AFTER}, - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Parameters(index = "0..*", - paramLabel = "KEY") - List keys = new ArrayList<>(); - - @CommandLine.Option(names = {OPT_WITH_KEY_PASSWORD}, - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @Override - public void run() { - Decrypt decrypt = throwIfUnsupportedSubcommand( - SopCLI.getSop().decrypt(), "decrypt"); - - throwIfOutputExists(verifyOut); - throwIfOutputExists(sessionKeyOut); - - setNotAfter(notAfter, decrypt); - setNotBefore(notBefore, decrypt); - setWithPasswords(withPassword, decrypt); - setWithSessionKeys(withSessionKey, decrypt); - setWithKeyPassword(withKeyPassword, decrypt); - setVerifyWith(certs, decrypt); - setDecryptWith(keys, decrypt); - - if (verifyOut != null && certs.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.option_requires_other_option", OPT_VERIFICATIONS_OUT, OPT_VERIFY_WITH); - throw new SOPGPException.IncompleteVerification(errorMsg); - } - - try { - ReadyWithResult ready = decrypt.ciphertext(System.in); - DecryptionResult result = ready.writeTo(System.out); - writeSessionKeyOut(result); - writeVerifyOut(result); - - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (SOPGPException.CannotDecrypt e) { - String errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message"); - throw new SOPGPException.CannotDecrypt(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - - private void writeVerifyOut(DecryptionResult result) throws IOException { - if (verifyOut != null) { - if (result.getVerifications().isEmpty()) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg); - } - - try (OutputStream fileOut = getOutput(verifyOut)) { - PrintWriter writer = new PrintWriter(fileOut); - for (Verification verification : result.getVerifications()) { - // CHECKSTYLE:OFF - writer.println(verification.toString()); - // CHECKSTYLE:ON - } - writer.flush(); - } - } - } - - private void writeSessionKeyOut(DecryptionResult result) throws IOException { - if (sessionKeyOut == null) { - return; - } - try (OutputStream outputStream = getOutput(sessionKeyOut)) { - if (!result.getSessionKey().isPresent()) { - String errorMsg = getMsg("sop.error.runtime.no_session_key_extracted"); - throw new SOPGPException.UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)); - } - SessionKey sessionKey = result.getSessionKey().get(); - PrintWriter writer = new PrintWriter(outputStream); - // CHECKSTYLE:OFF - writer.println(sessionKey.toString()); - // CHECKSTYLE:ON - writer.flush(); - } - } - - private void setDecryptWith(List keys, Decrypt decrypt) { - for (String key : keys) { - try (InputStream keyIn = getInput(key)) { - decrypt.withKey(keyIn); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", key); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setVerifyWith(List certs, Decrypt decrypt) { - for (String cert : certs) { - try (InputStream certIn = getInput(cert)) { - decrypt.verifyWithCert(certIn); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", cert); - throw new SOPGPException.BadData(errorMsg, badData); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } - } - } - - private void setWithSessionKeys(List withSessionKey, Decrypt decrypt) { - for (String sessionKeyFile : withSessionKey) { - String sessionKeyString; - try { - sessionKeyString = stringFromInputStream(getInput(sessionKeyFile)); - } catch (IOException e) { - throw new RuntimeException(e); - } - SessionKey sessionKey; - try { - sessionKey = SessionKey.fromString(sessionKeyString); - } catch (IllegalArgumentException e) { - String errorMsg = getMsg("sop.error.input.malformed_session_key"); - throw new IllegalArgumentException(errorMsg, e); - } - try { - decrypt.withSessionKey(sessionKey); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - } - - private void setWithPasswords(List withPassword, Decrypt decrypt) { - for (String passwordFile : withPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - decrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setWithKeyPassword(List withKeyPassword, Decrypt decrypt) { - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - decrypt.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - private void setNotAfter(String notAfter, Decrypt decrypt) { - Date notAfterDate = parseNotAfter(notAfter); - try { - decrypt.verifyNotAfter(notAfterDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - private void setNotBefore(String notBefore, Decrypt decrypt) { - Date notBeforeDate = parseNotBefore(notBefore); - try { - decrypt.verifyNotBefore(notBeforeDate); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt new file mode 100644 index 0000000..95298c2 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -0,0 +1,223 @@ +package sop.cli.picocli.commands + +import java.io.IOException +import java.io.PrintWriter +import picocli.CommandLine.* +import sop.DecryptionResult +import sop.SessionKey +import sop.SessionKey.Companion.fromString +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* +import sop.operation.Decrypt + +@Command( + name = "decrypt", + resourceBundle = "msg_decrypt", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class DecryptCmd : AbstractSopCmd() { + + @Option(names = [OPT_SESSION_KEY_OUT], paramLabel = "SESSIONKEY") + var sessionKeyOut: String? = null + + @Option(names = [OPT_WITH_SESSION_KEY], paramLabel = "SESSIONKEY") + var withSessionKey: List = listOf() + + @Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD") + var withPassword: List = listOf() + + @Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS") + var verifyOut: String? = null + + @Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List = listOf() + + @Option(names = [OPT_NOT_BEFORE], paramLabel = "DATE") var notBefore = "-" + + @Option(names = [OPT_NOT_AFTER], paramLabel = "DATE") var notAfter = "now" + + @Parameters(index = "0..*", paramLabel = "KEY") var keys: List = listOf() + + @Option(names = [OPT_WITH_KEY_PASSWORD], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + override fun run() { + val decrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().decrypt(), "decrypt") + + throwIfOutputExists(verifyOut) + throwIfOutputExists(sessionKeyOut) + + setNotAfter(notAfter, decrypt) + setNotBefore(notBefore, decrypt) + setWithPasswords(withPassword, decrypt) + setWithSessionKeys(withSessionKey, decrypt) + setWithKeyPassword(withKeyPassword, decrypt) + setVerifyWith(certs, decrypt) + setDecryptWith(keys, decrypt) + + if (verifyOut != null && certs.isEmpty()) { + val errorMsg = + getMsg( + "sop.error.usage.option_requires_other_option", + OPT_VERIFICATIONS_OUT, + OPT_VERIFY_WITH) + throw IncompleteVerification(errorMsg) + } + + try { + val ready = decrypt.ciphertext(System.`in`) + val result = ready.writeTo(System.out) + writeSessionKeyOut(result) + writeVerifyOut(result) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } catch (e: CannotDecrypt) { + val errorMsg = getMsg("sop.error.runtime.cannot_decrypt_message") + throw CannotDecrypt(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } + } + + @Throws(IOException::class) + private fun writeVerifyOut(result: DecryptionResult) { + verifyOut?.let { + if (result.verifications.isEmpty()) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg) + } + + getOutput(verifyOut).use { out -> + PrintWriter(out).use { pw -> result.verifications.forEach { pw.println(it) } } + } + } + } + + @Throws(IOException::class) + private fun writeSessionKeyOut(result: DecryptionResult) { + sessionKeyOut?.let { fileName -> + getOutput(fileName).use { out -> + if (!result.sessionKey.isPresent) { + val errorMsg = getMsg("sop.error.runtime.no_session_key_extracted") + throw UnsupportedOption(String.format(errorMsg, OPT_SESSION_KEY_OUT)) + } + + PrintWriter(out).use { it.println(result.sessionKey.get()!!) } + } + } + } + + private fun setDecryptWith(keys: List, decrypt: Decrypt) { + for (key in keys) { + try { + getInput(key).use { decrypt.withKey(it) } + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", key) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", key) + throw BadData(errorMsg, badData) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setVerifyWith(certs: List, decrypt: Decrypt) { + for (cert in certs) { + try { + getInput(cert).use { certIn -> decrypt.verifyWithCert(certIn) } + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", cert) + throw BadData(errorMsg, badData) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } + } + } + + private fun setWithSessionKeys(withSessionKey: List, decrypt: Decrypt) { + for (sessionKeyFile in withSessionKey) { + val sessionKeyString: String = + try { + stringFromInputStream(getInput(sessionKeyFile)) + } catch (e: IOException) { + throw RuntimeException(e) + } + val sessionKey: SessionKey = + try { + fromString(sessionKeyString) + } catch (e: IllegalArgumentException) { + val errorMsg = getMsg("sop.error.input.malformed_session_key") + throw IllegalArgumentException(errorMsg, e) + } + try { + decrypt.withSessionKey(sessionKey) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_SESSION_KEY) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + } + + private fun setWithPasswords(withPassword: List, decrypt: Decrypt) { + for (passwordFile in withPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + decrypt.withPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_PASSWORD) + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setWithKeyPassword(withKeyPassword: List, decrypt: Decrypt) { + for (passwordFile in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + decrypt.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", OPT_WITH_KEY_PASSWORD) + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } + + private fun setNotAfter(notAfter: String, decrypt: Decrypt) { + val notAfterDate = parseNotAfter(notAfter) + try { + decrypt.verifyNotAfter(notAfterDate) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_AFTER) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + private fun setNotBefore(notBefore: String, decrypt: Decrypt) { + val notBeforeDate = parseNotBefore(notBefore) + try { + decrypt.verifyNotBefore(notBeforeDate) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", OPT_NOT_BEFORE) + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + companion object { + const val OPT_SESSION_KEY_OUT = "--session-key-out" + const val OPT_WITH_SESSION_KEY = "--with-session-key" + const val OPT_WITH_PASSWORD = "--with-password" + const val OPT_WITH_KEY_PASSWORD = "--with-key-password" + const val OPT_VERIFICATIONS_OUT = "--verifications-out" + const val OPT_VERIFY_WITH = "--verify-with" + const val OPT_NOT_BEFORE = "--verify-not-before" + const val OPT_NOT_AFTER = "--verify-not-after" + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index e3dd198..c2c67ba 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -278,7 +278,7 @@ public class DecryptCmdTest { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); File existingVerifyOut = File.createTempFile("existing-verify-out", ".tmp"); - SopCLI.main(new String[] {"decrypt", "--verify-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", existingVerifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); } @Test @@ -302,7 +302,7 @@ public class DecryptCmdTest { } }); - SopCLI.main(new String[] {"decrypt", "--verify-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", verifyOut.getAbsolutePath(), "--verify-with", certFile.getAbsolutePath()}); try (BufferedReader reader = new BufferedReader(new FileReader(verifyOut))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); @@ -377,6 +377,6 @@ public class DecryptCmdTest { @Test @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verify-out", "out.file"}); + SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); } } From 714c933cef399b3c98101c51b14d4f86e45fb407 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 18:31:52 +0100 Subject: [PATCH 060/238] Kotlin conversion: InlineDetachCmd --- .../cli/picocli/commands/InlineDetachCmd.java | 50 ------------------- .../cli/picocli/commands/InlineDetachCmd.kt | 47 +++++++++++++++++ 2 files changed, 47 insertions(+), 50 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java deleted file mode 100644 index 52b654f..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineDetachCmd.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Signatures; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineDetach; - -import java.io.IOException; -import java.io.OutputStream; - -@CommandLine.Command(name = "inline-detach", - resourceBundle = "msg_inline-detach", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineDetachCmd extends AbstractSopCmd { - - @CommandLine.Option( - names = {"--signatures-out"}, - paramLabel = "SIGNATURES") - String signaturesOut; - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @Override - public void run() { - InlineDetach inlineDetach = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineDetach(), "inline-detach"); - - throwIfOutputExists(signaturesOut); - throwIfMissingArg(signaturesOut, "--signatures-out"); - - if (!armor) { - inlineDetach.noArmor(); - } - - try (OutputStream outputStream = getOutput(signaturesOut)) { - Signatures signatures = inlineDetach - .message(System.in).writeTo(System.out); - signatures.writeTo(outputStream); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt new file mode 100644 index 0000000..e311adf --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineDetachCmd.kt @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.lang.RuntimeException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException + +@Command( + name = "inline-detach", + resourceBundle = "msg_inline-detach", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class InlineDetachCmd : AbstractSopCmd() { + + @Option(names = ["--signatures-out"], paramLabel = "SIGNATURES") + var signaturesOut: String? = null + + @Option(names = ["--no-armor"], negatable = true) var armor: Boolean = true + + override fun run() { + val inlineDetach = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineDetach(), "inline-detach") + + throwIfOutputExists(signaturesOut) + throwIfMissingArg(signaturesOut, "--signatures-out") + + if (!armor) { + inlineDetach.noArmor() + } + + try { + getOutput(signaturesOut).use { sigOut -> + inlineDetach + .message(System.`in`) + .writeTo(System.out) // message out + .writeTo(sigOut) // signatures out + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 159ffbe0846960f419ba743a744f000012adcd49 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 4 Nov 2023 18:32:01 +0100 Subject: [PATCH 061/238] Add missing license headers --- .../src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt | 4 ++++ .../src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt | 4 ++++ .../src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt index cb6bc79..ccfba4d 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.cli.picocli.commands import java.io.IOException diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt index 9ae97c6..09d2a71 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DearmorCmd.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.cli.picocli.commands import java.io.IOException diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt index 95298c2..1e688b3 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.cli.picocli.commands import java.io.IOException From bfad8c4203258563492323dd2bbbf2646a72e2de Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:21:42 +0100 Subject: [PATCH 062/238] Kotlin conversion: EncryptCmd --- .../sop/cli/picocli/commands/EncryptCmd.java | 154 ------------------ .../sop/cli/picocli/commands/EncryptCmd.kt | 138 ++++++++++++++++ 2 files changed, 138 insertions(+), 154 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java deleted file mode 100644 index efda26f..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/EncryptCmd.java +++ /dev/null @@ -1,154 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.operation.Encrypt; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "encrypt", - resourceBundle = "msg_encrypt", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class EncryptCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = {"--as"}, - paramLabel = "{binary|text}") - EncryptAs type; - - @CommandLine.Option(names = "--with-password", - paramLabel = "PASSWORD") - List withPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--sign-with", - paramLabel = "KEY") - List signWith = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @CommandLine.Option(names = "--profile", - paramLabel = "PROFILE") - String profile; - - @CommandLine.Parameters(index = "0..*", - paramLabel = "CERTS") - List certs = new ArrayList<>(); - - @Override - public void run() { - Encrypt encrypt = throwIfUnsupportedSubcommand( - SopCLI.getSop().encrypt(), "encrypt"); - - if (profile != null) { - try { - encrypt.profile(profile); - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", profile); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } - - if (type != null) { - try { - encrypt.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - if (withPassword.isEmpty() && certs.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.password_or_cert_required"); - throw new SOPGPException.MissingArg(errorMsg); - } - - for (String passwordFileName : withPassword) { - try { - String password = stringFromInputStream(getInput(passwordFileName)); - encrypt.withPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String passwordFileName : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFileName)); - encrypt.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : signWith) { - try (InputStream keyIn = getInput(keyInput)) { - encrypt.signWith(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected keyIsProtected) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, keyIsProtected); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.KeyCannotSign keyCannotSign) { - String errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput); - throw new SOPGPException.KeyCannotSign(errorMsg, keyCannotSign); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - for (String certInput : certs) { - try (InputStream certIn = getInput(certInput)) { - encrypt.withCert(certIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.CertCannotEncrypt certCannotEncrypt) { - String errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput); - throw new SOPGPException.CertCannotEncrypt(errorMsg, certCannotEncrypt); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - encrypt.noArmor(); - } - - try { - Ready ready = encrypt.plaintext(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt new file mode 100644 index 0000000..b3b0d87 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt @@ -0,0 +1,138 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.EncryptAs +import sop.exception.SOPGPException.* + +@Command( + name = "encrypt", + resourceBundle = "msg_encrypt", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class EncryptCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--as"], paramLabel = "{binary|text}") var type: EncryptAs? = null + + @Option(names = ["--with-password"], paramLabel = "PASSWORD") + var withPassword: List = listOf() + + @Option(names = ["--sign-with"], paramLabel = "KEY") var signWith: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null + + @Parameters(index = "0..*", paramLabel = "CERTS") var certs: List = listOf() + + override fun run() { + val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt") + + profile?.let { + try { + encrypt.profile(it) + } catch (e: UnsupportedProfile) { + val errorMsg = getMsg("sop.error.usage.profile_not_supported", "encrypt", it) + throw UnsupportedProfile(errorMsg, e) + } + } + + type?.let { + try { + encrypt.mode(it) + } catch (e: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") + throw UnsupportedOption(errorMsg, e) + } + } + + if (withPassword.isEmpty() && certs.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.password_or_cert_required") + throw MissingArg(errorMsg) + } + + for (passwordFileName in withPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + encrypt.withPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (passwordFileName in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFileName)) + encrypt.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in signWith) { + try { + getInput(keyInput).use { keyIn -> encrypt.signWith(keyIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (keyIsProtected: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) + throw KeyIsProtected(errorMsg, keyIsProtected) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg("sop.error.runtime.key_uses_unsupported_asymmetric_algorithm", keyInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (keyCannotSign: KeyCannotSign) { + val errorMsg = getMsg("sop.error.runtime.key_cannot_sign", keyInput) + throw KeyCannotSign(errorMsg, keyCannotSign) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + for (certInput in certs) { + try { + getInput(certInput).use { certIn -> encrypt.withCert(certIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg( + "sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (certCannotEncrypt: CertCannotEncrypt) { + val errorMsg = getMsg("sop.error.runtime.cert_cannot_encrypt", certInput) + throw CertCannotEncrypt(errorMsg, certCannotEncrypt) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + encrypt.noArmor() + } + + try { + val ready = encrypt.plaintext(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 019dd63e1b6d4ee56d4d9c02328310ff8d213d16 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:26:06 +0100 Subject: [PATCH 063/238] Kotlin conversion: ExtractCertCmd --- .../cli/picocli/commands/ExtractCertCmd.java | 43 ------------------- .../cli/picocli/commands/ExtractCertCmd.kt | 40 +++++++++++++++++ 2 files changed, 40 insertions(+), 43 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java deleted file mode 100644 index 64a7a84..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ExtractCertCmd.java +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import java.io.IOException; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.ExtractCert; - -@CommandLine.Command(name = "extract-cert", - resourceBundle = "msg_extract-cert", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class ExtractCertCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @Override - public void run() { - ExtractCert extractCert = throwIfUnsupportedSubcommand( - SopCLI.getSop().extractCert(), "extract-cert"); - - if (!armor) { - extractCert.noArmor(); - } - - try { - Ready ready = extractCert.key(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_private_key"); - throw new SOPGPException.BadData(errorMsg, badData); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt new file mode 100644 index 0000000..cff996f --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ExtractCertCmd.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import sop.exception.SOPGPException.BadData + +@Command( + name = "extract-cert", + resourceBundle = "msg_extract-cert", + exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) +class ExtractCertCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + override fun run() { + val extractCert = + throwIfUnsupportedSubcommand(SopCLI.getSop().extractCert(), "extract-cert") + + if (!armor) { + extractCert.noArmor() + } + + try { + val ready = extractCert.key(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_private_key") + throw BadData(errorMsg, badData) + } + } +} From e9a5467f6b885b44248ee6b4555e3f991769d9b3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:33:34 +0100 Subject: [PATCH 064/238] Kotlin conversion: GenerateKeyCmd --- .../cli/picocli/commands/GenerateKeyCmd.java | 85 ------------------- .../cli/picocli/commands/GenerateKeyCmd.kt | 76 +++++++++++++++++ 2 files changed, 76 insertions(+), 85 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java deleted file mode 100644 index eea992e..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/GenerateKeyCmd.java +++ /dev/null @@ -1,85 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.GenerateKey; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "generate-key", - resourceBundle = "msg_generate-key", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class GenerateKeyCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Parameters(paramLabel = "USERID") - List userId = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - String withKeyPassword; - - @CommandLine.Option(names = "--profile", - paramLabel = "PROFILE") - String profile; - - @CommandLine.Option(names = "--signing-only") - boolean signingOnly = false; - - @Override - public void run() { - GenerateKey generateKey = throwIfUnsupportedSubcommand( - SopCLI.getSop().generateKey(), "generate-key"); - - if (profile != null) { - try { - generateKey.profile(profile); - } catch (SOPGPException.UnsupportedProfile e) { - String errorMsg = getMsg("sop.error.usage.profile_not_supported", "generate-key", profile); - throw new SOPGPException.UnsupportedProfile(errorMsg, e); - } - } - - if (signingOnly) { - generateKey.signingOnly(); - } - - for (String userId : userId) { - generateKey.userId(userId); - } - - if (!armor) { - generateKey.noArmor(); - } - - if (withKeyPassword != null) { - try { - String password = stringFromInputStream(getInput(withKeyPassword)); - generateKey.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption e) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, e); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - try { - Ready ready = generateKey.generate(); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt new file mode 100644 index 0000000..fb5e321 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.UnsupportedOption +import sop.exception.SOPGPException.UnsupportedProfile + +@Command( + name = "generate-key", + resourceBundle = "msg_generate-key", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class GenerateKeyCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Parameters(paramLabel = "USERID") var userId: List = listOf() + + @Option(names = ["---with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: String? = null + + @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null + + @Option(names = ["--signing-only"]) var signingOnly: Boolean = false + + override fun run() { + val generateKey = + throwIfUnsupportedSubcommand(SopCLI.getSop().generateKey(), "generate-key") + + profile?.let { + try { + generateKey.profile(it) + } catch (e: UnsupportedProfile) { + val errorMsg = + getMsg("sop.error.usage.profile_not_supported", "generate-key", profile!!) + throw UnsupportedProfile(errorMsg, e) + } + } + + if (signingOnly) { + generateKey.signingOnly() + } + + for (userId in userId) { + generateKey.userId(userId) + } + + if (!armor) { + generateKey.noArmor() + } + + withKeyPassword?.let { + try { + val password = stringFromInputStream(getInput(it)) + generateKey.withKeyPassword(password) + } catch (e: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, e) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + try { + val ready = generateKey.generate() + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 2e118357e2cee22e65ef999e94c830d70e48ad54 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:40:46 +0100 Subject: [PATCH 065/238] Kotlin conversion: InlineSignCmd --- .../cli/picocli/commands/InlineSignCmd.java | 101 ------------------ .../sop/cli/picocli/commands/InlineSignCmd.kt | 89 +++++++++++++++ 2 files changed, 89 insertions(+), 101 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java deleted file mode 100644 index 1865bcf..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineSignCmd.java +++ /dev/null @@ -1,101 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.Ready; -import sop.cli.picocli.SopCLI; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.operation.InlineSign; - -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-sign", - resourceBundle = "msg_inline-sign", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineSignCmd extends AbstractSopCmd { - - @CommandLine.Option(names = "--no-armor", - negatable = true) - boolean armor = true; - - @CommandLine.Option(names = "--as", - paramLabel = "{binary|text|clearsigned}") - InlineSignAs type; - - @CommandLine.Parameters(paramLabel = "KEYS") - List secretKeyFile = new ArrayList<>(); - - @CommandLine.Option(names = "--with-key-password", - paramLabel = "PASSWORD") - List withKeyPassword = new ArrayList<>(); - - @Override - public void run() { - InlineSign inlineSign = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineSign(), "inline-sign"); - - // Clearsigned messages are inherently armored, so --no-armor makes no sense. - if (!armor && type == InlineSignAs.clearsigned) { - String errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor"); - throw new SOPGPException.IncompatibleOptions(errorMsg); - } - - if (type != null) { - try { - inlineSign.mode(type); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - if (secretKeyFile.isEmpty()) { - String errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS"); - throw new SOPGPException.MissingArg(errorMsg); - } - - for (String passwordFile : withKeyPassword) { - try { - String password = stringFromInputStream(getInput(passwordFile)); - inlineSign.withKeyPassword(password); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - for (String keyInput : secretKeyFile) { - try (InputStream keyIn = getInput(keyInput)) { - inlineSign.key(keyIn); - } catch (IOException e) { - throw new RuntimeException(e); - } catch (SOPGPException.KeyIsProtected e) { - String errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput); - throw new SOPGPException.KeyIsProtected(errorMsg, e); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - if (!armor) { - inlineSign.noArmor(); - } - - try { - Ready ready = inlineSign.data(System.in); - ready.writeTo(System.out); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt new file mode 100644 index 0000000..c41f6f6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineSignCmd.kt @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.enums.InlineSignAs +import sop.exception.SOPGPException.* + +@Command( + name = "inline-sign", + resourceBundle = "msg_inline-sign", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineSignCmd : AbstractSopCmd() { + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--as"], paramLabel = "{binary|text|clearsigned}") + var type: InlineSignAs? = null + + @Parameters(paramLabel = "KEYS") var secretKeyFile: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + override fun run() { + val inlineSign = throwIfUnsupportedSubcommand(SopCLI.getSop().inlineSign(), "inline-sign") + + if (!armor && type == InlineSignAs.clearsigned) { + val errorMsg = getMsg("sop.error.usage.incompatible_options.clearsigned_no_armor") + throw IncompatibleOptions(errorMsg) + } + + type?.let { + try { + inlineSign.mode(it) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--as") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + } + + if (secretKeyFile.isEmpty()) { + val errorMsg = getMsg("sop.error.usage.parameter_required", "KEYS") + throw MissingArg(errorMsg) + } + + for (passwordFile in withKeyPassword) { + try { + val password = stringFromInputStream(getInput(passwordFile)) + inlineSign.withKeyPassword(password) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + throw UnsupportedOption(errorMsg, unsupportedOption) + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + for (keyInput in secretKeyFile) { + try { + getInput(keyInput).use { keyIn -> inlineSign.key(keyIn) } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: KeyIsProtected) { + val errorMsg = getMsg("sop.error.runtime.cannot_unlock_key", keyInput) + throw KeyIsProtected(errorMsg, e) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_private_key", keyInput) + throw BadData(errorMsg, badData) + } + } + + if (!armor) { + inlineSign.noArmor() + } + + try { + val ready = inlineSign.data(System.`in`) + ready.writeTo(System.out) + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From b884f2b1a9447064d17f6944d1dcaf0fcd43c3c8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:49:04 +0100 Subject: [PATCH 066/238] Kotlin conversion: InlineVerifyCmd --- .../cli/picocli/commands/InlineVerifyCmd.java | 108 ------------------ .../cli/picocli/commands/package-info.java | 8 -- .../cli/picocli/commands/InlineVerifyCmd.kt | 93 +++++++++++++++ 3 files changed, 93 insertions(+), 116 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java deleted file mode 100644 index c413c85..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/InlineVerifyCmd.java +++ /dev/null @@ -1,108 +0,0 @@ -// SPDX-FileCopyrightText: 2022 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli.commands; - -import picocli.CommandLine; -import sop.ReadyWithResult; -import sop.Verification; -import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; -import sop.operation.InlineVerify; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -@CommandLine.Command(name = "inline-verify", - resourceBundle = "msg_inline-verify", - exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) -public class InlineVerifyCmd extends AbstractSopCmd { - - @CommandLine.Parameters(arity = "0..*", - paramLabel = "CERT") - List certificates = new ArrayList<>(); - - @CommandLine.Option(names = {"--not-before"}, - paramLabel = "DATE") - String notBefore = "-"; - - @CommandLine.Option(names = {"--not-after"}, - paramLabel = "DATE") - String notAfter = "now"; - - @CommandLine.Option(names = "--verifications-out", paramLabel = "VERIFICATIONS") - String verificationsOut; - - @Override - public void run() { - InlineVerify inlineVerify = throwIfUnsupportedSubcommand( - SopCLI.getSop().inlineVerify(), "inline-verify"); - - throwIfOutputExists(verificationsOut); - - if (notAfter != null) { - try { - inlineVerify.notAfter(parseNotAfter(notAfter)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - if (notBefore != null) { - try { - inlineVerify.notBefore(parseNotBefore(notBefore)); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - String errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before"); - throw new SOPGPException.UnsupportedOption(errorMsg, unsupportedOption); - } - } - - for (String certInput : certificates) { - try (InputStream certIn = getInput(certInput)) { - inlineVerify.cert(certIn); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.UnsupportedAsymmetricAlgo unsupportedAsymmetricAlgo) { - String errorMsg = getMsg("sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput); - throw new SOPGPException.UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.not_a_certificate", certInput); - throw new SOPGPException.BadData(errorMsg, badData); - } - } - - List verifications = null; - try { - ReadyWithResult> ready = inlineVerify.data(System.in); - verifications = ready.writeTo(System.out); - } catch (SOPGPException.NoSignature e) { - String errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found"); - throw new SOPGPException.NoSignature(errorMsg, e); - } catch (IOException ioException) { - throw new RuntimeException(ioException); - } catch (SOPGPException.BadData badData) { - String errorMsg = getMsg("sop.error.input.stdin_not_a_message"); - throw new SOPGPException.BadData(errorMsg, badData); - } - - if (verificationsOut != null) { - try (OutputStream outputStream = getOutput(verificationsOut)) { - PrintWriter pw = new PrintWriter(outputStream); - for (Verification verification : verifications) { - // CHECKSTYLE:OFF - pw.println(verification); - // CHECKSTYLE:ON - } - pw.flush(); - pw.close(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java deleted file mode 100644 index fc6aefd..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Subcommands of the PGPainless SOP. - */ -package sop.cli.picocli.commands; diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt new file mode 100644 index 0000000..6a641a6 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/InlineVerifyCmd.kt @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import java.io.PrintWriter +import picocli.CommandLine.* +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* + +@Command( + name = "inline-verify", + resourceBundle = "msg_inline-verify", + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) +class InlineVerifyCmd : AbstractSopCmd() { + + @Parameters(arity = "0..*", paramLabel = "CERT") var certificates: List = listOf() + + @Option(names = ["--not-before"], paramLabel = "DATE") var notBefore: String = "-" + + @Option(names = ["--not-after"], paramLabel = "DATE") var notAfter: String = "now" + + @Option(names = ["--verifications-out"], paramLabel = "VERIFICATIONS") + var verificationsOut: String? = null + + override fun run() { + val inlineVerify = + throwIfUnsupportedSubcommand(SopCLI.getSop().inlineVerify(), "inline-verify") + + throwIfOutputExists(verificationsOut) + + try { + inlineVerify.notAfter(parseNotAfter(notAfter)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-after") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + try { + inlineVerify.notBefore(parseNotBefore(notBefore)) + } catch (unsupportedOption: UnsupportedOption) { + val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--not-before") + throw UnsupportedOption(errorMsg, unsupportedOption) + } + + for (certInput in certificates) { + try { + getInput(certInput).use { certIn -> inlineVerify.cert(certIn) } + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (unsupportedAsymmetricAlgo: UnsupportedAsymmetricAlgo) { + val errorMsg = + getMsg( + "sop.error.runtime.cert_uses_unsupported_asymmetric_algorithm", certInput) + throw UnsupportedAsymmetricAlgo(errorMsg, unsupportedAsymmetricAlgo) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.not_a_certificate", certInput) + throw BadData(errorMsg, badData) + } + } + + val verifications = + try { + val ready = inlineVerify.data(System.`in`) + ready.writeTo(System.out) + } catch (e: NoSignature) { + val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") + throw NoSignature(errorMsg, e) + } catch (ioException: IOException) { + throw RuntimeException(ioException) + } catch (badData: BadData) { + val errorMsg = getMsg("sop.error.input.stdin_not_a_message") + throw BadData(errorMsg, badData) + } + + verificationsOut?.let { + try { + getOutput(it).use { outputStream -> + val pw = PrintWriter(outputStream) + for (verification in verifications) { + pw.println(verification) + } + pw.flush() + pw.close() + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + } +} From b251956f492268eadb7f1876e671a34b87c38b9e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:49:45 +0100 Subject: [PATCH 067/238] Delete unused Print class --- .../src/main/java/sop/cli/picocli/Print.java | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/Print.java diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java b/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java deleted file mode 100644 index 9e81b66..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/Print.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -public class Print { - - public static void outln(String string) { - // CHECKSTYLE:OFF - System.out.println(string); - // CHECKSTYLE:ON - } -} From 5c2695228b6e780d6cbcb9959c580732c76a3af2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:55:50 +0100 Subject: [PATCH 068/238] Kotlin conversion: SOPExceptionExitCodeMapper --- .../picocli/SOPExceptionExitCodeMapper.java | 34 ------------------- .../cli/picocli/SOPExceptionExitCodeMapper.kt | 31 +++++++++++++++++ 2 files changed, 31 insertions(+), 34 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java deleted file mode 100644 index 8b38af3..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExceptionExitCodeMapper.java +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; -import sop.exception.SOPGPException; - -public class SOPExceptionExitCodeMapper implements CommandLine.IExitCodeExceptionMapper { - - @Override - public int getExitCode(Throwable exception) { - if (exception instanceof SOPGPException) { - return ((SOPGPException) exception).getExitCode(); - } - if (exception instanceof CommandLine.UnmatchedArgumentException) { - CommandLine.UnmatchedArgumentException ex = (CommandLine.UnmatchedArgumentException) exception; - // Unmatched option of subcommand (eg. `generate-key -k`) - if (ex.isUnknownOption()) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - // Unmatched subcommand - return SOPGPException.UnsupportedSubcommand.EXIT_CODE; - } - // Invalid option (eg. `--label Invalid`) - if (exception instanceof CommandLine.ParameterException) { - return SOPGPException.UnsupportedOption.EXIT_CODE; - } - - // Others, like IOException etc. - return 1; - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt new file mode 100644 index 0000000..29aa77b --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import picocli.CommandLine.* +import sop.exception.SOPGPException + +class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper { + + override fun getExitCode(exception: Throwable): Int = + if (exception is SOPGPException) { + // SOPGPExceptions have well-defined exit code + exception.getExitCode() + } else if (exception is UnmatchedArgumentException) { + if (exception.isUnknownOption) { + // Unmatched option of subcommand (e.g. `generate-key --unknown`) + SOPGPException.UnsupportedOption.EXIT_CODE + } else { + // Unmatched subcommand + SOPGPException.UnsupportedSubcommand.EXIT_CODE + } + } else if (exception is ParameterException) { + // Invalid option (e.g. `--as invalid`) + SOPGPException.UnsupportedOption.EXIT_CODE + } else { + // Others, like IOException etc. + 1 + } +} From 0c2cf5cb19dec00e9c9de41ffb7307fd859fc113 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 11:59:11 +0100 Subject: [PATCH 069/238] Kotlin conversion: SOPExecutionExceptionHandler --- .../picocli/SOPExecutionExceptionHandler.java | 33 ------------------ .../picocli/SOPExecutionExceptionHandler.kt | 34 +++++++++++++++++++ 2 files changed, 34 insertions(+), 33 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java deleted file mode 100644 index f6906ff..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SOPExecutionExceptionHandler.java +++ /dev/null @@ -1,33 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.CommandLine; - -public class SOPExecutionExceptionHandler implements CommandLine.IExecutionExceptionHandler { - - @Override - public int handleExecutionException(Exception ex, CommandLine commandLine, CommandLine.ParseResult parseResult) { - - int exitCode = commandLine.getExitCodeExceptionMapper() != null ? - commandLine.getExitCodeExceptionMapper().getExitCode(ex) : - commandLine.getCommandSpec().exitCodeOnExecutionException(); - - CommandLine.Help.ColorScheme colorScheme = commandLine.getColorScheme(); - // CHECKSTYLE:OFF - if (ex.getMessage() != null) { - commandLine.getErr().println(colorScheme.errorText(ex.getMessage())); - } else { - commandLine.getErr().println(ex.getClass().getName()); - } - - if (SopCLI.stacktrace) { - ex.printStackTrace(commandLine.getErr()); - } - // CHECKSTYLE:ON - - return exitCode; - } -} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt new file mode 100644 index 0000000..52236d3 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExecutionExceptionHandler.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import picocli.CommandLine +import picocli.CommandLine.IExecutionExceptionHandler + +class SOPExecutionExceptionHandler : IExecutionExceptionHandler { + override fun handleExecutionException( + ex: Exception, + commandLine: CommandLine, + parseResult: CommandLine.ParseResult + ): Int { + val exitCode = + if (commandLine.exitCodeExceptionMapper != null) + commandLine.exitCodeExceptionMapper.getExitCode(ex) + else commandLine.commandSpec.exitCodeOnExecutionException() + + val colorScheme = commandLine.colorScheme + if (ex.message != null) { + commandLine.getErr().println(colorScheme.errorText(ex.message)) + } else { + commandLine.getErr().println(ex.javaClass.getName()) + } + + if (SopCLI.stacktrace) { + ex.printStackTrace(commandLine.getErr()) + } + + return exitCode + } +} From baa44a6b1a4218a051ea8ee1045736224beea20f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 12:35:23 +0100 Subject: [PATCH 070/238] Kotlin conversion: SopCLI --- .../src/main/java/sop/cli/picocli/SopCLI.java | 129 ------------------ .../java/sop/cli/picocli/package-info.java | 8 -- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 113 +++++++++++++++ .../test/java/sop/cli/picocli/SOPTest.java | 2 +- 4 files changed, 114 insertions(+), 138 deletions(-) delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java delete mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java b/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java deleted file mode 100644 index 5420dea..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/SopCLI.java +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.cli.picocli; - -import picocli.AutoComplete; -import picocli.CommandLine; -import sop.SOP; -import sop.cli.picocli.commands.ArmorCmd; -import sop.cli.picocli.commands.ChangeKeyPasswordCmd; -import sop.cli.picocli.commands.DearmorCmd; -import sop.cli.picocli.commands.DecryptCmd; -import sop.cli.picocli.commands.InlineDetachCmd; -import sop.cli.picocli.commands.EncryptCmd; -import sop.cli.picocli.commands.ExtractCertCmd; -import sop.cli.picocli.commands.GenerateKeyCmd; -import sop.cli.picocli.commands.InlineSignCmd; -import sop.cli.picocli.commands.InlineVerifyCmd; -import sop.cli.picocli.commands.ListProfilesCmd; -import sop.cli.picocli.commands.RevokeKeyCmd; -import sop.cli.picocli.commands.SignCmd; -import sop.cli.picocli.commands.VerifyCmd; -import sop.cli.picocli.commands.VersionCmd; -import sop.exception.SOPGPException; - -import java.util.List; -import java.util.Locale; -import java.util.ResourceBundle; - -@CommandLine.Command( - name = "sop", - resourceBundle = "msg_sop", - exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, - subcommands = { - // Meta Subcommands - VersionCmd.class, - ListProfilesCmd.class, - // Key and Certificate Management Subcommands - GenerateKeyCmd.class, - ChangeKeyPasswordCmd.class, - RevokeKeyCmd.class, - ExtractCertCmd.class, - // Messaging Subcommands - SignCmd.class, - VerifyCmd.class, - EncryptCmd.class, - DecryptCmd.class, - InlineDetachCmd.class, - InlineSignCmd.class, - InlineVerifyCmd.class, - // Transport Subcommands - ArmorCmd.class, - DearmorCmd.class, - // Miscellaneous Subcommands - CommandLine.HelpCommand.class, - AutoComplete.GenerateCompletion.class - } -) -public class SopCLI { - // Singleton - static SOP SOP_INSTANCE; - static ResourceBundle cliMsg = ResourceBundle.getBundle("msg_sop"); - - public static String EXECUTABLE_NAME = "sop"; - - @CommandLine.Option(names = {"--stacktrace"}, - scope = CommandLine.ScopeType.INHERIT) - static boolean stacktrace; - - public static void main(String[] args) { - int exitCode = execute(args); - if (exitCode != 0) { - System.exit(exitCode); - } - } - - public static int execute(String[] args) { - - // Set locale - new CommandLine(new InitLocale()).parseArgs(args); - - // get error message bundle - cliMsg = ResourceBundle.getBundle("msg_sop"); - - // Prepare CLI - CommandLine cmd = new CommandLine(SopCLI.class); - - // explicitly set help command resource bundle - cmd.getSubcommands().get("help").setResourceBundle(ResourceBundle.getBundle("msg_help")); - - // Hide generate-completion command - cmd.getSubcommands().get("generate-completion").getCommandSpec().usageMessage().hidden(true); - - cmd.setCommandName(EXECUTABLE_NAME) - .setExecutionExceptionHandler(new SOPExecutionExceptionHandler()) - .setExitCodeExceptionMapper(new SOPExceptionExitCodeMapper()) - .setCaseInsensitiveEnumValuesAllowed(true); - - return cmd.execute(args); - } - - public static SOP getSop() { - if (SOP_INSTANCE == null) { - String errorMsg = cliMsg.getString("sop.error.runtime.no_backend_set"); - throw new IllegalStateException(errorMsg); - } - return SOP_INSTANCE; - } - - public static void setSopInstance(SOP instance) { - SOP_INSTANCE = instance; - } -} - -/** - * Control the locale. - * - * @see Picocli Readme - */ -class InitLocale { - @CommandLine.Option(names = { "-l", "--locale" }, descriptionKey = "sop.locale") - void setLocale(String locale) { - Locale.setDefault(new Locale(locale)); - } - - @CommandLine.Unmatched - List remainder; // ignore any other parameters and options in the first parsing phase -} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java b/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java deleted file mode 100644 index 83f426d..0000000 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of the Stateless OpenPGP Command Line Interface using Picocli. - */ -package sop.cli.picocli; diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt new file mode 100644 index 0000000..1d5d46b --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli + +import java.util.* +import kotlin.system.exitProcess +import picocli.AutoComplete.GenerateCompletion +import picocli.CommandLine +import picocli.CommandLine.* +import sop.SOP +import sop.cli.picocli.commands.* +import sop.exception.SOPGPException + +@Command( + name = "sop", + resourceBundle = "msg_sop", + exitCodeOnInvalidInput = SOPGPException.UnsupportedSubcommand.EXIT_CODE, + subcommands = + [ + // Meta subcommands + VersionCmd::class, + ListProfilesCmd::class, + // Key and certificate management + GenerateKeyCmd::class, + ChangeKeyPasswordCmd::class, + RevokeKeyCmd::class, + ExtractCertCmd::class, + // Messaging subcommands + SignCmd::class, + VerifyCmd::class, + EncryptCmd::class, + DecryptCmd::class, + InlineDetachCmd::class, + InlineSignCmd::class, + InlineVerifyCmd::class, + // Transport + ArmorCmd::class, + DearmorCmd::class, + // misc + HelpCommand::class, + GenerateCompletion::class]) +class SopCLI { + + companion object { + @JvmStatic private var sopInstance: SOP? = null + + @JvmStatic + fun getSop(): SOP = + checkNotNull(sopInstance) { cliMsg.getString("sop.error.runtime.no_backend_set") } + + @JvmStatic + fun setSopInstance(sop: SOP?) { + sopInstance = sop + } + + @JvmField var cliMsg: ResourceBundle = ResourceBundle.getBundle("msg_sop") + + @JvmField var EXECUTABLE_NAME = "sop" + + @JvmField + @Option(names = ["--stacktrace"], 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(SopCLI::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 Picocli Readme + */ + @Command + class InitLocale { + @Option(names = ["-l", "--locale"], descriptionKey = "sop.locale") + fun setLocale(locale: String) = Locale.setDefault(Locale(locale)) + + @Unmatched + var remainder: MutableList = + mutableListOf() // ignore any other parameters and options in the first parsing phase + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index 47b6123..68b32be 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -45,7 +45,7 @@ public class SOPTest { @Test @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { - SopCLI.SOP_INSTANCE = null; + SopCLI.setSopInstance(null); // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) SopCLI.main(new String[] {"armor"}); } From edef89907410621cbfa89553d3c3d93167096dc6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 12:42:41 +0100 Subject: [PATCH 071/238] Fix GenerateKey --with-key-password option name --- .../src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt index fb5e321..7fa5a70 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/GenerateKeyCmd.kt @@ -20,7 +20,7 @@ class GenerateKeyCmd : AbstractSopCmd() { @Parameters(paramLabel = "USERID") var userId: List = listOf() - @Option(names = ["---with-key-password"], paramLabel = "PASSWORD") + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") var withKeyPassword: String? = null @Option(names = ["--profile"], paramLabel = "PROFILE") var profile: String? = null From 41acdfe03a9ee0b8e6732402ae58145220764218 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 12:42:56 +0100 Subject: [PATCH 072/238] ProxyOutputStream: Extend OutputStream --- .../src/main/kotlin/sop/util/ProxyOutputStream.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index 142f7b3..da6c4fa 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -15,7 +15,7 @@ import java.io.OutputStream * 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. */ -class ProxyOutputStream { +class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null @@ -27,7 +27,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(b: ByteArray) { + override fun write(b: ByteArray) { if (swapped == null) { buffer.write(b) } else { @@ -37,7 +37,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(b: ByteArray, off: Int, len: Int) { + override fun write(b: ByteArray, off: Int, len: Int) { if (swapped == null) { buffer.write(b, off, len) } else { @@ -47,7 +47,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun flush() { + override fun flush() { buffer.flush() if (swapped != null) { swapped!!.flush() @@ -56,7 +56,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun close() { + override fun close() { buffer.close() if (swapped != null) { swapped!!.close() @@ -65,7 +65,7 @@ class ProxyOutputStream { @Synchronized @Throws(IOException::class) - fun write(i: Int) { + override fun write(i: Int) { if (swapped == null) { buffer.write(i) } else { From 0563105b1f071389c431cb6e05df1283366163c3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 13:38:02 +0100 Subject: [PATCH 073/238] Bump version to 8.0.0-SNAPSHOT --- README.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6dd9a81..fa7e5a5 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0 # SOP for Java [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) -[![Spec Revision: 7](https://img.shields.io/badge/Spec%20Revision-7-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/07/) +[![Spec Revision: 8](https://img.shields.io/badge/Spec%20Revision-8-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/08/) [![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) diff --git a/version.gradle b/version.gradle index d8ddfd6..c5a9cb1 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '7.0.1' + shortVersion = '8.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From 2051c3632a3fd8a5fbd2438a8a5a2b91c2819878 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 13:52:36 +0100 Subject: [PATCH 074/238] external-sop: Mark methods with @Nonnull where applicable --- .../main/java/sop/external/ExternalSOP.java | 19 +++++++++++-- .../sop/external/operation/ArmorExternal.java | 8 ++++-- .../operation/ChangeKeyPasswordExternal.java | 11 +++++--- .../external/operation/DearmorExternal.java | 4 ++- .../external/operation/DecryptExternal.java | 27 ++++++++++++------- .../operation/DetachedSignExternal.java | 16 +++++++---- .../operation/DetachedVerifyExternal.java | 16 +++++++---- .../external/operation/EncryptExternal.java | 25 +++++++++++------ .../operation/ExtractCertExternal.java | 5 +++- .../operation/GenerateKeyExternal.java | 13 ++++++--- .../operation/InlineDetachExternal.java | 9 ++++--- .../operation/InlineSignExternal.java | 14 +++++++--- .../operation/InlineVerifyExternal.java | 15 +++++++---- .../operation/ListProfilesExternal.java | 4 ++- .../external/operation/RevokeKeyExternal.java | 8 ++++-- .../external/operation/VersionExternal.java | 6 +++++ 16 files changed, 146 insertions(+), 54 deletions(-) diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index e1479ee..3819848 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -106,76 +106,91 @@ public class ExternalSOP implements SOP { } @Override + @Nonnull public Version version() { return new VersionExternal(binaryName, properties); } @Override + @Nonnull public GenerateKey generateKey() { return new GenerateKeyExternal(binaryName, properties); } @Override + @Nonnull public ExtractCert extractCert() { return new ExtractCertExternal(binaryName, properties); } @Override + @Nonnull public DetachedSign detachedSign() { return new DetachedSignExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public InlineSign inlineSign() { return new InlineSignExternal(binaryName, properties); } @Override + @Nonnull public DetachedVerify detachedVerify() { return new DetachedVerifyExternal(binaryName, properties); } @Override + @Nonnull public InlineVerify inlineVerify() { return new InlineVerifyExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public InlineDetach inlineDetach() { return new InlineDetachExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public Encrypt encrypt() { return new EncryptExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public Decrypt decrypt() { return new DecryptExternal(binaryName, properties, tempDirProvider); } @Override + @Nonnull public Armor armor() { return new ArmorExternal(binaryName, properties); } @Override + @Nonnull public ListProfiles listProfiles() { return new ListProfilesExternal(binaryName, properties); } @Override + @Nonnull public RevokeKey revokeKey() { return new RevokeKeyExternal(binaryName, properties); } @Override + @Nonnull public ChangeKeyPassword changeKeyPassword() { return new ChangeKeyPasswordExternal(binaryName, properties); } @Override + @Nonnull public Dearmor dearmor() { return new DearmorExternal(binaryName, properties); } @@ -349,7 +364,7 @@ public class ExternalSOP implements SOP { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = stdIn.read(buf)) >= 0) { @@ -388,7 +403,7 @@ public class ExternalSOP implements SOP { return new Ready() { @Override - public void writeTo(OutputStream outputStream) throws IOException { + public void writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = standardIn.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java index 4df7fca..e1d02e9 100644 --- a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java @@ -10,6 +10,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Armor; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -30,13 +31,16 @@ public class ArmorExternal implements Armor { } @Override - public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption { + @Deprecated + @Nonnull + public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption { commandList.add("--label=" + label); return this; } @Override - public Ready data(InputStream data) throws SOPGPException.BadData { + @Nonnull + public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java index 210152f..d53d6a5 100644 --- a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.ChangeKeyPassword; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -27,13 +28,15 @@ public class ChangeKeyPasswordExternal implements ChangeKeyPassword { } @Override + @Nonnull public ChangeKeyPassword noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public ChangeKeyPassword oldKeyPassphrase(String oldPassphrase) { + @Nonnull + public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) { this.commandList.add("--old-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + oldPassphrase); keyPasswordCounter++; @@ -42,7 +45,8 @@ public class ChangeKeyPasswordExternal implements ChangeKeyPassword { } @Override - public ChangeKeyPassword newKeyPassphrase(String newPassphrase) { + @Nonnull + public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) { this.commandList.add("--new-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + newPassphrase); keyPasswordCounter++; @@ -51,7 +55,8 @@ public class ChangeKeyPasswordExternal implements ChangeKeyPassword { } @Override - public Ready keys(InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { + @Nonnull + public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, inputStream); } } diff --git a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java index bedf018..cd3da6f 100644 --- a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Dearmor; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -29,7 +30,8 @@ public class DearmorExternal implements Dearmor { } @Override - public Ready data(InputStream data) throws SOPGPException.BadData { + @Nonnull + public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java index 0b91d5b..a1c4016 100644 --- a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java @@ -13,6 +13,7 @@ import sop.external.ExternalSOP; import sop.operation.Decrypt; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -48,21 +49,24 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt verifyNotBefore(Date timestamp) + @Nonnull + public Decrypt verifyNotBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { this.commandList.add("--verify-not-before=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public Decrypt verifyNotAfter(Date timestamp) + @Nonnull + public Decrypt verifyNotAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { this.commandList.add("--verify-not-after=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public Decrypt verifyWithCert(InputStream cert) + @Nonnull + public Decrypt verifyWithCert(@Nonnull InputStream cert) throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "VERIFY_WITH_" + verifyWithCounter++; commandList.add("--verify-with=@ENV:" + envVar); @@ -71,7 +75,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withSessionKey(SessionKey sessionKey) + @Nonnull + public Decrypt withSessionKey(@Nonnull SessionKey sessionKey) throws SOPGPException.UnsupportedOption { String envVar = "SESSION_KEY_" + withSessionKeyCounter++; commandList.add("--with-session-key=@ENV:" + envVar); @@ -80,7 +85,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withPassword(String password) + @Nonnull + public Decrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { String envVar = "PASSWORD_" + withPasswordCounter++; commandList.add("--with-password=@ENV:" + envVar); @@ -89,7 +95,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withKey(InputStream key) + @Nonnull + public Decrypt withKey(@Nonnull InputStream key) throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "KEY_" + keyCounter++; commandList.add("@ENV:" + envVar); @@ -98,7 +105,8 @@ public class DecryptExternal implements Decrypt { } @Override - public Decrypt withKeyPassword(byte[] password) + @Nonnull + public Decrypt withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); @@ -107,7 +115,8 @@ public class DecryptExternal implements Decrypt { } @Override - public ReadyWithResult ciphertext(InputStream ciphertext) + @Nonnull + public ReadyWithResult ciphertext(@Nonnull InputStream ciphertext) throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt, SOPGPException.KeyIsProtected, IOException { File tempDir = tempDirProvider.provideTempDirectory(); @@ -131,7 +140,7 @@ public class DecryptExternal implements Decrypt { return new ReadyWithResult() { @Override - public DecryptionResult writeTo(OutputStream outputStream) throws IOException { + public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = ciphertext.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java index 7c579d4..2ef2714 100644 --- a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java @@ -12,6 +12,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.DetachedSign; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -43,13 +44,15 @@ public class DetachedSignExternal implements DetachedSign { } @Override + @Nonnull public DetachedSign noArmor() { commandList.add("--no-armor"); return this; } @Override - public DetachedSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { + @Nonnull + public DetachedSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "KEY_" + keyCounter++; commandList.add("@ENV:" + envVar); envList.add(envVar + "=" + ExternalSOP.readString(key)); @@ -57,7 +60,8 @@ public class DetachedSignExternal implements DetachedSign { } @Override - public DetachedSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { + @Nonnull + public DetachedSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); envList.add(envVar + "=" + new String(password)); @@ -65,13 +69,15 @@ public class DetachedSignExternal implements DetachedSign { } @Override - public DetachedSign mode(SignAs mode) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedSign mode(@Nonnull SignAs mode) throws SOPGPException.UnsupportedOption { commandList.add("--as=" + mode); return this; } @Override - public ReadyWithResult data(InputStream data) + @Nonnull + public ReadyWithResult data(@Nonnull InputStream data) throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { File tempDir = tempDirProvider.provideTempDirectory(); @@ -88,7 +94,7 @@ public class DetachedSignExternal implements DetachedSign { return new ReadyWithResult() { @Override - public SigningResult writeTo(OutputStream outputStream) throws IOException { + public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = data.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java index 2f19c5b..866150d 100644 --- a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java @@ -11,6 +11,7 @@ import sop.operation.DetachedVerify; import sop.operation.VerifySignatures; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -42,31 +43,36 @@ public class DetachedVerifyExternal implements DetachedVerify { } @Override - public DetachedVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public DetachedVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public DetachedVerify cert(InputStream cert) throws SOPGPException.BadData { + @Nonnull + public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData { this.certs.add(cert); return this; } @Override - public VerifySignatures signatures(InputStream signatures) throws SOPGPException.BadData { + @Nonnull + public VerifySignatures signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData { this.signatures = signatures; return this; } @Override - public List data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + @Nonnull + public List data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { commandList.add("@ENV:SIGNATURE"); envList.add("SIGNATURE=" + ExternalSOP.readString(signatures)); diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java index 3eabfc5..f41a36e 100644 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java @@ -12,6 +12,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.Encrypt; +import javax.annotation.Nonnull; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -42,20 +43,23 @@ public class EncryptExternal implements Encrypt { } @Override + @Nonnull public Encrypt noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public Encrypt mode(EncryptAs mode) + @Nonnull + public Encrypt mode(@Nonnull EncryptAs mode) throws SOPGPException.UnsupportedOption { this.commandList.add("--as=" + mode); return this; } @Override - public Encrypt signWith(InputStream key) + @Nonnull + public Encrypt signWith(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { String envVar = "SIGN_WITH_" + SIGN_WITH_COUNTER++; @@ -65,7 +69,8 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt withKeyPassword(byte[] password) + @Nonnull + public Encrypt withKeyPassword(@Nonnull byte[] password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { String envVar = "KEY_PASSWORD_" + KEY_PASSWORD_COUNTER++; commandList.add("--with-key-password=@ENV:" + envVar); @@ -74,7 +79,8 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt withPassword(String password) + @Nonnull + public Encrypt withPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { String envVar = "PASSWORD_" + PASSWORD_COUNTER++; commandList.add("--with-password=@ENV:" + envVar); @@ -83,7 +89,8 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt withCert(InputStream cert) + @Nonnull + public Encrypt withCert(@Nonnull InputStream cert) throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { String envVar = "CERT_" + CERT_COUNTER++; @@ -93,13 +100,15 @@ public class EncryptExternal implements Encrypt { } @Override - public Encrypt profile(String profileName) { + @Nonnull + public Encrypt profile(@Nonnull String profileName) { commandList.add("--profile=" + profileName); return this; } @Override - public ReadyWithResult plaintext(InputStream plaintext) + @Nonnull + public ReadyWithResult plaintext(@Nonnull InputStream plaintext) throws SOPGPException.KeyIsProtected, IOException { File tempDir = tempDirProvider.provideTempDirectory(); @@ -116,7 +125,7 @@ public class EncryptExternal implements Encrypt { return new ReadyWithResult() { @Override - public EncryptionResult writeTo(OutputStream outputStream) throws IOException { + public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = plaintext.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java index 5fdcdc1..538359a 100644 --- a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.ExtractCert; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -29,13 +30,15 @@ public class ExtractCertExternal implements ExtractCert { } @Override + @Nonnull public ExtractCert noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public Ready key(InputStream keyInputStream) throws SOPGPException.BadData { + @Nonnull + public Ready key(@Nonnull InputStream keyInputStream) throws SOPGPException.BadData { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keyInputStream); } } diff --git a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java index c46dfb3..a8244cb 100644 --- a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.GenerateKey; +import javax.annotation.Nonnull; import java.util.ArrayList; import java.util.List; import java.util.Properties; @@ -30,19 +31,22 @@ public class GenerateKeyExternal implements GenerateKey { } @Override + @Nonnull public GenerateKey noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public GenerateKey userId(String userId) { + @Nonnull + public GenerateKey userId(@Nonnull String userId) { this.commandList.add(userId); return this; } @Override - public GenerateKey withKeyPassword(String password) + @Nonnull + public GenerateKey withKeyPassword(@Nonnull String password) throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { this.commandList.add("--with-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + password); @@ -52,18 +56,21 @@ public class GenerateKeyExternal implements GenerateKey { } @Override - public GenerateKey profile(String profile) { + @Nonnull + public GenerateKey profile(@Nonnull String profile) { commandList.add("--profile=" + profile); return this; } @Override + @Nonnull public GenerateKey signingOnly() { commandList.add("--signing-only"); return this; } @Override + @Nonnull public Ready generate() throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { return ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList); diff --git a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java b/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java index c03fe1b..a798006 100644 --- a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java +++ b/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java @@ -10,6 +10,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.InlineDetach; +import javax.annotation.Nonnull; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -37,13 +38,15 @@ public class InlineDetachExternal implements InlineDetach { } @Override + @Nonnull public InlineDetach noArmor() { commandList.add("--no-armor"); return this; } @Override - public ReadyWithResult message(InputStream messageInputStream) throws IOException, SOPGPException.BadData { + @Nonnull + public ReadyWithResult message(@Nonnull InputStream messageInputStream) throws IOException, SOPGPException.BadData { File tempDir = tempDirProvider.provideTempDirectory(); File signaturesOut = new File(tempDir, "signatures"); @@ -60,7 +63,7 @@ public class InlineDetachExternal implements InlineDetach { return new ReadyWithResult() { @Override - public Signatures writeTo(OutputStream outputStream) throws IOException { + public Signatures writeTo(@Nonnull OutputStream outputStream) throws IOException { byte[] buf = new byte[4096]; int r; while ((r = messageInputStream.read(buf)) > 0) { @@ -90,7 +93,7 @@ public class InlineDetachExternal implements InlineDetach { final byte[] sigBytes = signaturesBuffer.toByteArray(); return new Signatures() { @Override - public void writeTo(OutputStream signatureOutputStream) throws IOException { + public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException { signatureOutputStream.write(sigBytes); } }; diff --git a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java index 68a630d..1a86002 100644 --- a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java +++ b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java @@ -10,6 +10,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.InlineSign; +import javax.annotation.Nonnull; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -34,13 +35,15 @@ public class InlineSignExternal implements InlineSign { } @Override + @Nonnull public InlineSign noArmor() { commandList.add("--no-armor"); return this; } @Override - public InlineSign key(InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { + @Nonnull + public InlineSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { String envVar = "KEY_" + keyCounter++; commandList.add("@ENV:" + envVar); envList.add(envVar + "=" + ExternalSOP.readString(key)); @@ -48,7 +51,8 @@ public class InlineSignExternal implements InlineSign { } @Override - public InlineSign withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { + @Nonnull + public InlineSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); envList.add(envVar + "=" + new String(password)); @@ -56,13 +60,15 @@ public class InlineSignExternal implements InlineSign { } @Override - public InlineSign mode(InlineSignAs mode) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption { commandList.add("--as=" + mode); return this; } @Override - public Ready data(InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { + @Nonnull + public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); } } diff --git a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java index 8010367..10a2d47 100644 --- a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java @@ -11,6 +11,7 @@ import sop.external.ExternalSOP; import sop.operation.InlineVerify; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; @@ -42,19 +43,22 @@ public class InlineVerifyExternal implements InlineVerify { } @Override - public InlineVerify notBefore(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public InlineVerify notAfter(Date timestamp) throws SOPGPException.UnsupportedOption { + @Nonnull + public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); return this; } @Override - public InlineVerify cert(InputStream cert) throws SOPGPException.BadData, IOException { + @Nonnull + public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { String envVar = "CERT_" + certCounter++; commandList.add("@ENV:" + envVar); envList.add(envVar + "=" + ExternalSOP.readString(cert)); @@ -62,7 +66,8 @@ public class InlineVerifyExternal implements InlineVerify { } @Override - public ReadyWithResult> data(InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { + @Nonnull + public ReadyWithResult> data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { File tempDir = tempDirProvider.provideTempDirectory(); File verificationsOut = new File(tempDir, "verifications-out"); @@ -79,7 +84,7 @@ public class InlineVerifyExternal implements InlineVerify { return new ReadyWithResult>() { @Override - public List writeTo(OutputStream outputStream) throws IOException, SOPGPException.NoSignature { + public List writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { byte[] buf = new byte[4096]; int r; while ((r = data.read(buf)) > 0) { diff --git a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java index 0c76b63..21d5e13 100644 --- a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java +++ b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java @@ -8,6 +8,7 @@ import sop.Profile; import sop.external.ExternalSOP; import sop.operation.ListProfiles; +import javax.annotation.Nonnull; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -25,7 +26,8 @@ public class ListProfilesExternal implements ListProfiles { } @Override - public List subcommand(String command) { + @Nonnull + public List subcommand(@Nonnull String command) { commandList.add(command); try { String output = new String(ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList).getBytes()); diff --git a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java index e7aad9d..72ed549 100644 --- a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java +++ b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java @@ -9,6 +9,7 @@ import sop.exception.SOPGPException; import sop.external.ExternalSOP; import sop.operation.RevokeKey; +import javax.annotation.Nonnull; import java.io.InputStream; import java.util.ArrayList; import java.util.List; @@ -28,13 +29,15 @@ public class RevokeKeyExternal implements RevokeKey { } @Override + @Nonnull public RevokeKey noArmor() { this.commandList.add("--no-armor"); return this; } @Override - public RevokeKey withKeyPassword(byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { + @Nonnull + public RevokeKey withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; commandList.add("--with-key-password=@ENV:" + envVar); envList.add(envVar + "=" + new String(password)); @@ -42,7 +45,8 @@ public class RevokeKeyExternal implements RevokeKey { } @Override - public Ready keys(InputStream keys) { + @Nonnull + public Ready keys(@Nonnull InputStream keys) { return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys); } } diff --git a/external-sop/src/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java index 0b9c5b4..ab2bbef 100644 --- a/external-sop/src/main/java/sop/external/operation/VersionExternal.java +++ b/external-sop/src/main/java/sop/external/operation/VersionExternal.java @@ -7,6 +7,7 @@ package sop.external.operation; import sop.external.ExternalSOP; import sop.operation.Version; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; @@ -27,6 +28,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getName() { String[] command = new String[] {binary, "version"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -45,6 +47,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getVersion() { String[] command = new String[] {binary, "version"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -63,6 +66,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getBackendVersion() { String[] command = new String[] {binary, "version", "--backend"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -82,6 +86,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getExtendedVersion() { String[] command = new String[] {binary, "version", "--extended"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); @@ -137,6 +142,7 @@ public class VersionExternal implements Version { } @Override + @Nonnull public String getSopSpecVersion() { String[] command = new String[] {binary, "version", "--sop-spec"}; String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); From 3dde1748805f4c665fc29fb6ab816a8f490eb464 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 14:26:24 +0100 Subject: [PATCH 075/238] Fix woodpecker pipeline --- .woodpecker/.build.yml | 6 +++--- .woodpecker/.reuse.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.woodpecker/.build.yml b/.woodpecker/.build.yml index d72d19e..ff59c4e 100644 --- a/.woodpecker/.build.yml +++ b/.woodpecker/.build.yml @@ -1,6 +1,6 @@ -pipeline: +steps: run: - image: gradle:7.5-jdk8-jammy + image: gradle:7.6-jdk11-jammy commands: # Install Sequoia-SOP - apt update && apt install --yes sqop @@ -14,4 +14,4 @@ pipeline: - gradle check javadocAll # Code has coverage - gradle jacocoRootReport coveralls - secrets: [COVERALLS_REPO_TOKEN] + secrets: [coveralls_repo_token] diff --git a/.woodpecker/.reuse.yml b/.woodpecker/.reuse.yml index 58f17e6..d78c61e 100644 --- a/.woodpecker/.reuse.yml +++ b/.woodpecker/.reuse.yml @@ -1,6 +1,6 @@ # Code is licensed properly # See https://reuse.software/ -pipeline: +steps: reuse: image: fsfe/reuse:latest commands: From 03cabdf3fb0d0ac25c849480a47d5e255d219644 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 15:22:40 +0100 Subject: [PATCH 076/238] Add tests for --no-armor for change-key-password and revoke-key --- .../operation/ChangeKeyPasswordTest.java | 26 +++++++++++++++++++ .../testsuite/operation/RevokeKeyTest.java | 9 +++++++ 2 files changed, 35 insertions(+) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java index b575047..8948dda 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java @@ -10,6 +10,8 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; import sop.exception.SOPGPException; +import sop.testsuite.JUtils; +import sop.testsuite.TestData; import sop.util.UTF8Util; import java.io.IOException; @@ -17,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") @@ -95,4 +98,27 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { sop.changeKeyPassword().newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testNoArmor(SOP sop) throws IOException { + byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); + byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); + byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + + byte[] armored = sop.changeKeyPassword() + .oldKeyPassphrase(oldPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey) + .getBytes(); + JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + + byte[] unarmored = sop.changeKeyPassword() + .noArmor() + .oldKeyPassphrase(oldPassword) + .newKeyPassphrase(newPassword) + .keys(protectedKey) + .getBytes(); + assertFalse(JUtils.arrayStartsWith(unarmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); + } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java index 6595133..cb51332 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java @@ -43,6 +43,15 @@ public class RevokeKeyTest extends AbstractSOPTest { assertFalse(Arrays.equals(secretKey, revocation)); } + @ParameterizedTest + @MethodSource("provideInstances") + public void revokeUnprotectedKeyNoArmor(SOP sop) throws IOException { + byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); + byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes(); + + assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); + } + @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKeyUnarmored(SOP sop) throws IOException { From 802bc0aa73a4357912646b8e6cd9e2b0134cd021 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 15:24:09 +0100 Subject: [PATCH 077/238] ArmorCMD: Drop --label option --- .../kotlin/sop/cli/picocli/commands/ArmorCmd.kt | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt index ccfba4d..50716f1 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ArmorCmd.kt @@ -6,9 +6,7 @@ package sop.cli.picocli.commands import java.io.IOException import picocli.CommandLine.Command -import picocli.CommandLine.Option import sop.cli.picocli.SopCLI -import sop.enums.ArmorLabel import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.UnsupportedOption @@ -18,21 +16,9 @@ import sop.exception.SOPGPException.UnsupportedOption exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) class ArmorCmd : AbstractSopCmd() { - @Option(names = ["--label"], paramLabel = "{auto|sig|key|cert|message}") - var label: ArmorLabel? = null - override fun run() { val armor = throwIfUnsupportedSubcommand(SopCLI.getSop().armor(), "armor") - label?.let { - try { - armor.label(it) - } catch (unsupported: UnsupportedOption) { - val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--label") - throw UnsupportedOption(errorMsg, unsupported) - } - } - try { val ready = armor.data(System.`in`) ready.writeTo(System.out) From d24ff9cbde0520437f308ed61369294f81484a7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 15:26:06 +0100 Subject: [PATCH 078/238] Remove label related CLI tests --- .../cli/picocli/commands/ArmorCmdTest.java | 53 +++++-------------- 1 file changed, 12 insertions(+), 41 deletions(-) diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java index 6bdbe7f..da211e0 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java @@ -4,17 +4,6 @@ package sop.cli.picocli.commands; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; - import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import com.ginsberg.junit.exit.FailOnSystemExit; import org.junit.jupiter.api.BeforeEach; @@ -22,10 +11,20 @@ import org.junit.jupiter.api.Test; import sop.Ready; import sop.SOP; import sop.cli.picocli.SopCLI; -import sop.enums.ArmorLabel; import sop.exception.SOPGPException; import sop.operation.Armor; +import javax.annotation.Nonnull; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + public class ArmorCmdTest { private Armor armor; @@ -41,40 +40,12 @@ public class ArmorCmdTest { SopCLI.setSopInstance(sop); } - @Test - public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"armor"}); - verify(armor, never()).label(any()); - } - - @Test - public void assertLabelIsCalledWhenFlaggedWithArgument() throws SOPGPException.UnsupportedOption { - for (ArmorLabel label : ArmorLabel.values()) { - SopCLI.main(new String[] {"armor", "--label", label.name()}); - verify(armor, times(1)).label(label); - } - } - @Test public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { SopCLI.main(new String[] {"armor"}); verify(armor, times(1)).data((InputStream) any()); } - @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void assertThrowsForInvalidLabel() { - SopCLI.main(new String[] {"armor", "--label", "Invalid"}); - } - - @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void ifLabelsUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.label(any())).thenThrow(new SOPGPException.UnsupportedOption("Custom Armor labels are not supported.")); - - SopCLI.main(new String[] {"armor", "--label", "Sig"}); - } - @Test @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void ifBadDataExit41() throws SOPGPException.BadData, IOException { @@ -94,7 +65,7 @@ public class ArmorCmdTest { private static Ready nopReady() { return new Ready() { @Override - public void writeTo(OutputStream outputStream) { + public void writeTo(@Nonnull OutputStream outputStream) { } }; } From 1c0666b4e13a0199c5889fd422905a4af9059134 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:27:19 +0100 Subject: [PATCH 079/238] Kotlin conversion: ExternalSOP --- .../main/java/sop/external/ExternalSOP.java | 469 ------------------ .../main/kotlin/sop/external/ExternalSOP.kt | 318 ++++++++++++ 2 files changed, 318 insertions(+), 469 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/ExternalSOP.java create mode 100644 external-sop/src/main/kotlin/sop/external/ExternalSOP.kt diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java deleted file mode 100644 index 3819848..0000000 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ /dev/null @@ -1,469 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external; - -import sop.Ready; -import sop.SOP; -import sop.exception.SOPGPException; -import sop.external.operation.ArmorExternal; -import sop.external.operation.ChangeKeyPasswordExternal; -import sop.external.operation.DearmorExternal; -import sop.external.operation.DecryptExternal; -import sop.external.operation.DetachedSignExternal; -import sop.external.operation.DetachedVerifyExternal; -import sop.external.operation.EncryptExternal; -import sop.external.operation.ExtractCertExternal; -import sop.external.operation.GenerateKeyExternal; -import sop.external.operation.InlineDetachExternal; -import sop.external.operation.InlineSignExternal; -import sop.external.operation.InlineVerifyExternal; -import sop.external.operation.ListProfilesExternal; -import sop.external.operation.RevokeKeyExternal; -import sop.external.operation.VersionExternal; -import sop.operation.Armor; -import sop.operation.ChangeKeyPassword; -import sop.operation.Dearmor; -import sop.operation.Decrypt; -import sop.operation.DetachedSign; -import sop.operation.DetachedVerify; -import sop.operation.Encrypt; -import sop.operation.ExtractCert; -import sop.operation.GenerateKey; -import sop.operation.InlineDetach; -import sop.operation.InlineSign; -import sop.operation.InlineVerify; -import sop.operation.ListProfiles; -import sop.operation.RevokeKey; -import sop.operation.Version; - -import javax.annotation.Nonnull; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.attribute.FileAttribute; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link SOP} API using an external SOP binary. - */ -public class ExternalSOP implements SOP { - - private final String binaryName; - private final Properties properties; - private final TempDirProvider tempDirProvider; - - /** - * Instantiate an {@link ExternalSOP} object for the given binary and pass it empty environment variables, - * as well as a default {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - */ - public ExternalSOP(@Nonnull String binaryName) { - this(binaryName, new Properties()); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary, and pass it the given properties as - * environment variables, as well as a default {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - * @param properties environment variables - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties) { - this(binaryName, properties, defaultTempDirProvider()); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary and the given {@link TempDirProvider} - * using empty environment variables. - * - * @param binaryName name / path of the SOP binary - * @param tempDirProvider custom tempDirProvider - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull TempDirProvider tempDirProvider) { - this(binaryName, new Properties(), tempDirProvider); - } - - /** - * Instantiate an {@link ExternalSOP} object for the given binary using the given properties and - * custom {@link TempDirProvider}. - * - * @param binaryName name / path of the SOP binary - * @param properties environment variables - * @param tempDirProvider tempDirProvider - */ - public ExternalSOP(@Nonnull String binaryName, @Nonnull Properties properties, @Nonnull TempDirProvider tempDirProvider) { - this.binaryName = binaryName; - this.properties = properties; - this.tempDirProvider = tempDirProvider; - } - - @Override - @Nonnull - public Version version() { - return new VersionExternal(binaryName, properties); - } - - @Override - @Nonnull - public GenerateKey generateKey() { - return new GenerateKeyExternal(binaryName, properties); - } - - @Override - @Nonnull - public ExtractCert extractCert() { - return new ExtractCertExternal(binaryName, properties); - } - - @Override - @Nonnull - public DetachedSign detachedSign() { - return new DetachedSignExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public InlineSign inlineSign() { - return new InlineSignExternal(binaryName, properties); - } - - @Override - @Nonnull - public DetachedVerify detachedVerify() { - return new DetachedVerifyExternal(binaryName, properties); - } - - @Override - @Nonnull - public InlineVerify inlineVerify() { - return new InlineVerifyExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public InlineDetach inlineDetach() { - return new InlineDetachExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Encrypt encrypt() { - return new EncryptExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Decrypt decrypt() { - return new DecryptExternal(binaryName, properties, tempDirProvider); - } - - @Override - @Nonnull - public Armor armor() { - return new ArmorExternal(binaryName, properties); - } - - @Override - @Nonnull - public ListProfiles listProfiles() { - return new ListProfilesExternal(binaryName, properties); - } - - @Override - @Nonnull - public RevokeKey revokeKey() { - return new RevokeKeyExternal(binaryName, properties); - } - - @Override - @Nonnull - public ChangeKeyPassword changeKeyPassword() { - return new ChangeKeyPasswordExternal(binaryName, properties); - } - - @Override - @Nonnull - public Dearmor dearmor() { - return new DearmorExternal(binaryName, properties); - } - - public static void finish(@Nonnull Process process) throws IOException { - try { - mapExitCodeOrException(process); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } - } - - /** - * Wait for the {@link Process} to finish and read out its exit code. - * If the exit code is {@value "0"}, this method just returns. - * Otherwise, the exit code gets mapped to a {@link SOPGPException} which then gets thrown. - * If the exit code does not match any of the known exit codes defined in the SOP specification, - * this method throws a {@link RuntimeException} instead. - * - * @param process process - * @throws InterruptedException if the thread is interrupted before the process could exit - * @throws IOException in case of an IO error - */ - private static void mapExitCodeOrException(@Nonnull Process process) throws InterruptedException, IOException { - // wait for process termination - int exitCode = process.waitFor(); - - if (exitCode == 0) { - // we're good, bye - return; - } - - // Read error message - InputStream errIn = process.getErrorStream(); - String errorMessage = readString(errIn); - - switch (exitCode) { - case SOPGPException.NoSignature.EXIT_CODE: - throw new SOPGPException.NoSignature("External SOP backend reported error NoSignature (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE: - throw new UnsupportedOperationException("External SOP backend reported error UnsupportedAsymmetricAlgo (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.CertCannotEncrypt.EXIT_CODE: - throw new SOPGPException.CertCannotEncrypt("External SOP backend reported error CertCannotEncrypt (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.MissingArg.EXIT_CODE: - throw new SOPGPException.MissingArg("External SOP backend reported error MissingArg (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.IncompleteVerification.EXIT_CODE: - throw new SOPGPException.IncompleteVerification("External SOP backend reported error IncompleteVerification (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.CannotDecrypt.EXIT_CODE: - throw new SOPGPException.CannotDecrypt("External SOP backend reported error CannotDecrypt (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.PasswordNotHumanReadable.EXIT_CODE: - throw new SOPGPException.PasswordNotHumanReadable("External SOP backend reported error PasswordNotHumanReadable (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedOption.EXIT_CODE: - throw new SOPGPException.UnsupportedOption("External SOP backend reported error UnsupportedOption (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.BadData.EXIT_CODE: - throw new SOPGPException.BadData("External SOP backend reported error BadData (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.ExpectedText.EXIT_CODE: - throw new SOPGPException.ExpectedText("External SOP backend reported error ExpectedText (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.OutputExists.EXIT_CODE: - throw new SOPGPException.OutputExists("External SOP backend reported error OutputExists (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.MissingInput.EXIT_CODE: - throw new SOPGPException.MissingInput("External SOP backend reported error MissingInput (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.KeyIsProtected.EXIT_CODE: - throw new SOPGPException.KeyIsProtected("External SOP backend reported error KeyIsProtected (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedSubcommand.EXIT_CODE: - throw new SOPGPException.UnsupportedSubcommand("External SOP backend reported error UnsupportedSubcommand (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedSpecialPrefix.EXIT_CODE: - throw new SOPGPException.UnsupportedSpecialPrefix("External SOP backend reported error UnsupportedSpecialPrefix (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.AmbiguousInput.EXIT_CODE: - throw new SOPGPException.AmbiguousInput("External SOP backend reported error AmbiguousInput (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.KeyCannotSign.EXIT_CODE: - throw new SOPGPException.KeyCannotSign("External SOP backend reported error KeyCannotSign (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.IncompatibleOptions.EXIT_CODE: - throw new SOPGPException.IncompatibleOptions("External SOP backend reported error IncompatibleOptions (" + - exitCode + "):\n" + errorMessage); - - case SOPGPException.UnsupportedProfile.EXIT_CODE: - throw new SOPGPException.UnsupportedProfile("External SOP backend reported error UnsupportedProfile (" + - exitCode + "):\n" + errorMessage); - - default: - // Did you forget to add a case for a new exception type? - throw new RuntimeException("External SOP backend reported unknown exit code (" + - exitCode + "):\n" + errorMessage); - } - } - - /** - * Return all key-value pairs from the given {@link Properties} object as a list with items of the form - *
key=value
. - * - * @param properties properties - * @return list of key=value strings - */ - public static List propertiesToEnv(@Nonnull Properties properties) { - List env = new ArrayList<>(); - for (Object key : properties.keySet()) { - env.add(key + "=" + properties.get(key)); - } - return env; - } - - /** - * Read the contents of the {@link InputStream} and return them as a {@link String}. - * - * @param inputStream input stream - * @return string - * @throws IOException in case of an IO error - */ - public static String readString(@Nonnull InputStream inputStream) throws IOException { - ByteArrayOutputStream bOut = new ByteArrayOutputStream(); - byte[] buf = new byte[4096]; - int r; - while ((r = inputStream.read(buf)) > 0) { - bOut.write(buf, 0, r); - } - return bOut.toString(); - } - - /** - * Execute the given command on the given {@link Runtime} with the given list of environment variables. - * This command does not transform any input data, and instead is purely a producer. - * - * @param runtime runtime - * @param commandList command - * @param envList environment variables - * @return ready to read the result from - */ - public static Ready executeProducingOperation(@Nonnull Runtime runtime, - @Nonnull List commandList, - @Nonnull List envList) { - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = runtime.exec(command, env); - InputStream stdIn = process.getInputStream(); - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = stdIn.read(buf)) >= 0) { - outputStream.write(buf, 0, r); - } - - outputStream.flush(); - outputStream.close(); - - ExternalSOP.finish(process); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * Execute the given command on the given runtime using the given environment variables. - * The given input stream provides input for the process. - * This command is a transformation, meaning it is given input data and transforms it into output data. - * - * @param runtime runtime - * @param commandList command - * @param envList environment variables - * @param standardIn stream of input data for the process - * @return ready to read the result from - */ - public static Ready executeTransformingOperation(@Nonnull Runtime runtime, @Nonnull List commandList, @Nonnull List envList, @Nonnull InputStream standardIn) { - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new Ready() { - @Override - public void writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = standardIn.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - standardIn.close(); - - try { - processOut.flush(); - processOut.close(); - } catch (IOException e) { - // Perhaps the stream is already closed, in which case we ignore the exception. - if (!"Stream closed".equals(e.getMessage())) { - throw e; - } - } - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - processIn.close(); - - outputStream.flush(); - outputStream.close(); - - finish(process); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - /** - * This interface can be used to provide a directory in which external SOP binaries can temporarily store - * additional results of OpenPGP operations such that the binding classes can parse them out from there. - * Unfortunately, on Java you cannot open {@link java.io.FileDescriptor FileDescriptors} arbitrarily, so we - * have to rely on temporary files to pass results. - * An example: - *
sop decrypt
can emit signature verifications via
--verify-out=/path/to/tempfile
. - * {@link DecryptExternal} will then parse the temp file to make the result available to consumers. - * Temporary files are deleted after being read, yet creating temp files for sensitive information on disk - * might pose a security risk. Use with care! - */ - public interface TempDirProvider { - File provideTempDirectory() throws IOException; - } - - /** - * Default implementation of the {@link TempDirProvider} which stores temporary files in the systems temp dir - * ({@link Files#createTempDirectory(String, FileAttribute[])}). - * - * @return default implementation - */ - public static TempDirProvider defaultTempDirProvider() { - return new TempDirProvider() { - @Override - public File provideTempDirectory() throws IOException { - return Files.createTempDirectory("ext-sop").toFile(); - } - }; - } -} diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt new file mode 100644 index 0000000..3a0ef52 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -0,0 +1,318 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external + +import java.io.* +import java.nio.file.Files +import java.util.* +import javax.annotation.Nonnull +import sop.Ready +import sop.SOP +import sop.exception.SOPGPException.* +import sop.external.ExternalSOP.TempDirProvider +import sop.external.operation.* +import sop.operation.* + +/** + * Implementation of the {@link SOP} API using an external SOP binary. + * + * Instantiate an [ExternalSOP] object for the given binary and the given [TempDirProvider] using + * empty environment variables. + * + * @param binaryName name / path of the SOP binary + * @param tempDirProvider custom tempDirProvider + */ +class ExternalSOP( + private val binaryName: String, + private val properties: Properties = Properties(), + private val tempDirProvider: TempDirProvider = defaultTempDirProvider() +) : SOP { + + constructor( + binaryName: String, + properties: Properties + ) : this(binaryName, properties, defaultTempDirProvider()) + + override fun version(): Version = VersionExternal(binaryName, properties) + + override fun generateKey(): GenerateKey = GenerateKeyExternal(binaryName, properties) + + override fun extractCert(): ExtractCert = ExtractCertExternal(binaryName, properties) + + override fun detachedSign(): DetachedSign = + DetachedSignExternal(binaryName, properties, tempDirProvider) + + override fun inlineSign(): InlineSign = InlineSignExternal(binaryName, properties) + + override fun detachedVerify(): DetachedVerify = DetachedVerifyExternal(binaryName, properties) + + override fun inlineVerify(): InlineVerify = + InlineVerifyExternal(binaryName, properties, tempDirProvider) + + override fun inlineDetach(): InlineDetach = + InlineDetachExternal(binaryName, properties, tempDirProvider) + + override fun encrypt(): Encrypt = EncryptExternal(binaryName, properties, tempDirProvider) + + override fun decrypt(): Decrypt = DecryptExternal(binaryName, properties, tempDirProvider) + + override fun armor(): Armor = ArmorExternal(binaryName, properties) + + override fun dearmor(): Dearmor = DearmorExternal(binaryName, properties) + + override fun listProfiles(): ListProfiles = ListProfilesExternal(binaryName, properties) + + override fun revokeKey(): RevokeKey = RevokeKeyExternal(binaryName, properties) + + override fun changeKeyPassword(): ChangeKeyPassword = + ChangeKeyPasswordExternal(binaryName, properties) + + /** + * This interface can be used to provide a directory in which external SOP binaries can + * temporarily store additional results of OpenPGP operations such that the binding classes can + * parse them out from there. Unfortunately, on Java you cannot open + * [FileDescriptors][java.io.FileDescriptor] arbitrarily, so we have to rely on temporary files + * to pass results. An example: `sop decrypt` can emit signature verifications via + * `--verify-out=/path/to/tempfile`. [DecryptExternal] will then parse the temp file to make the + * result available to consumers. Temporary files are deleted after being read, yet creating + * temp files for sensitive information on disk might pose a security risk. Use with care! + */ + fun interface TempDirProvider { + + @Throws(IOException::class) fun provideTempDirectory(): File + } + + companion object { + + @JvmStatic + @Throws(IOException::class) + fun finish(process: Process) { + try { + mapExitCodeOrException(process) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + } + + @JvmStatic + @Throws(InterruptedException::class, IOException::class) + private fun mapExitCodeOrException(process: Process) { + // wait for process termination + val exitCode = process.waitFor() + + if (exitCode == 0) { + // we're good, bye + return + } + + // Read error message + val errIn = process.errorStream + val errorMessage = readString(errIn) + + when (exitCode) { + NoSignature.EXIT_CODE -> + throw NoSignature( + "External SOP backend reported error NoSignature ($exitCode):\n$errorMessage") + UnsupportedAsymmetricAlgo.EXIT_CODE -> + throw UnsupportedOperationException( + "External SOP backend reported error UnsupportedAsymmetricAlgo ($exitCode):\n$errorMessage") + CertCannotEncrypt.EXIT_CODE -> + throw CertCannotEncrypt( + "External SOP backend reported error CertCannotEncrypt ($exitCode):\n$errorMessage") + MissingArg.EXIT_CODE -> + throw MissingArg( + "External SOP backend reported error MissingArg ($exitCode):\n$errorMessage") + IncompleteVerification.EXIT_CODE -> + throw IncompleteVerification( + "External SOP backend reported error IncompleteVerification ($exitCode):\n$errorMessage") + CannotDecrypt.EXIT_CODE -> + throw CannotDecrypt( + "External SOP backend reported error CannotDecrypt ($exitCode):\n$errorMessage") + PasswordNotHumanReadable.EXIT_CODE -> + throw PasswordNotHumanReadable( + "External SOP backend reported error PasswordNotHumanReadable ($exitCode):\n$errorMessage") + UnsupportedOption.EXIT_CODE -> + throw UnsupportedOption( + "External SOP backend reported error UnsupportedOption ($exitCode):\n$errorMessage") + BadData.EXIT_CODE -> + throw BadData( + "External SOP backend reported error BadData ($exitCode):\n$errorMessage") + ExpectedText.EXIT_CODE -> + throw ExpectedText( + "External SOP backend reported error ExpectedText ($exitCode):\n$errorMessage") + OutputExists.EXIT_CODE -> + throw OutputExists( + "External SOP backend reported error OutputExists ($exitCode):\n$errorMessage") + MissingInput.EXIT_CODE -> + throw MissingInput( + "External SOP backend reported error MissingInput ($exitCode):\n$errorMessage") + KeyIsProtected.EXIT_CODE -> + throw KeyIsProtected( + "External SOP backend reported error KeyIsProtected ($exitCode):\n$errorMessage") + UnsupportedSubcommand.EXIT_CODE -> + throw UnsupportedSubcommand( + "External SOP backend reported error UnsupportedSubcommand ($exitCode):\n$errorMessage") + UnsupportedSpecialPrefix.EXIT_CODE -> + throw UnsupportedSpecialPrefix( + "External SOP backend reported error UnsupportedSpecialPrefix ($exitCode):\n$errorMessage") + AmbiguousInput.EXIT_CODE -> + throw AmbiguousInput( + "External SOP backend reported error AmbiguousInput ($exitCode):\n$errorMessage") + KeyCannotSign.EXIT_CODE -> + throw KeyCannotSign( + "External SOP backend reported error KeyCannotSign ($exitCode):\n$errorMessage") + IncompatibleOptions.EXIT_CODE -> + throw IncompatibleOptions( + "External SOP backend reported error IncompatibleOptions ($exitCode):\n$errorMessage") + UnsupportedProfile.EXIT_CODE -> + throw UnsupportedProfile( + "External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") + + // Did you forget to add a case for a new exception type? + else -> + throw RuntimeException( + "External SOP backend reported unknown exit code ($exitCode):\n$errorMessage") + } + } + + /** + * Return all key-value pairs from the given [Properties] object as a list with items of the + * form `key=value`. + * + * @param properties properties + * @return list of key=value strings + */ + @JvmStatic + fun propertiesToEnv(properties: Properties): List = + properties.map { "${it.key}=${it.value}" } + + /** + * Read the contents of the [InputStream] and return them as a [String]. + * + * @param inputStream input stream + * @return string + * @throws IOException in case of an IO error + */ + @JvmStatic + @Throws(IOException::class) + fun readString(inputStream: InputStream): String { + val bOut = ByteArrayOutputStream() + val buf = ByteArray(4096) + var r: Int + while (inputStream.read(buf).also { r = it } > 0) { + bOut.write(buf, 0, r) + } + return bOut.toString() + } + + /** + * Execute the given command on the given [Runtime] with the given list of environment + * variables. This command does not transform any input data, and instead is purely a + * producer. + * + * @param runtime runtime + * @param commandList command + * @param envList environment variables + * @return ready to read the result from + */ + @JvmStatic + fun executeProducingOperation( + runtime: Runtime, + commandList: List, + envList: List + ): Ready { + try { + val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray()) + val stdIn = process.inputStream + + return object : Ready() { + @Throws(IOException::class) + override fun writeTo(@Nonnull outputStream: OutputStream) { + val buf = ByteArray(4096) + var r: Int + while (stdIn.read(buf).also { r = it } >= 0) { + outputStream.write(buf, 0, r) + } + outputStream.flush() + outputStream.close() + finish(process) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + /** + * Execute the given command on the given runtime using the given environment variables. The + * given input stream provides input for the process. This command is a transformation, + * meaning it is given input data and transforms it into output data. + * + * @param runtime runtime + * @param commandList command + * @param envList environment variables + * @param standardIn stream of input data for the process + * @return ready to read the result from + */ + @JvmStatic + fun executeTransformingOperation( + runtime: Runtime, + commandList: List, + envList: List, + standardIn: InputStream + ): Ready { + try { + val process = runtime.exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : Ready() { + override fun writeTo(outputStream: OutputStream) { + val buf = ByteArray(4096) + var r: Int + while (standardIn.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + standardIn.close() + + try { + processOut.flush() + processOut.close() + } catch (e: IOException) { + // Perhaps the stream is already closed, in which case we ignore the + // exception. + if ("Stream closed" != e.message) { + throw e + } + } + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + processIn.close() + + outputStream.flush() + outputStream.close() + + finish(process) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + /** + * Default implementation of the [TempDirProvider] which stores temporary files in the + * systems temp dir ([Files.createTempDirectory]). + * + * @return default implementation + */ + @JvmStatic + fun defaultTempDirProvider(): TempDirProvider { + return TempDirProvider { Files.createTempDirectory("ext-sop").toFile() } + } + } +} From 67719526188a5ccbf1b4087fd4924c899b24703f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:33:44 +0100 Subject: [PATCH 080/238] Kotlin conversion: ArmorExternal --- .../sop/external/operation/ArmorExternal.java | 46 ------------------- .../sop/external/operation/ArmorExternal.kt | 26 +++++++++++ 2 files changed, 26 insertions(+), 46 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ArmorExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java b/external-sop/src/main/java/sop/external/operation/ArmorExternal.java deleted file mode 100644 index e1d02e9..0000000 --- a/external-sop/src/main/java/sop/external/operation/ArmorExternal.java +++ /dev/null @@ -1,46 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.enums.ArmorLabel; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Armor; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Armor} operation using an external SOP binary. - */ -public class ArmorExternal implements Armor { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public ArmorExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("armor"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Deprecated - @Nonnull - public Armor label(@Nonnull ArmorLabel label) throws SOPGPException.UnsupportedOption { - commandList.add("--label=" + label); - return this; - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt new file mode 100644 index 0000000..f80c57b --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.enums.ArmorLabel +import sop.exception.SOPGPException +import sop.external.ExternalSOP +import sop.operation.Armor + +/** Implementation of the [Armor] operation using an external SOP binary. */ +class ArmorExternal(binary: String, environment: Properties) : Armor { + + private val commandList: MutableList = mutableListOf(binary, "armor") + private val envList: List = ExternalSOP.propertiesToEnv(environment) + + override fun label(label: ArmorLabel): Armor = apply { commandList.add("--label=$label") } + + @Throws(SOPGPException.BadData::class) + override fun data(data: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) +} From d149aac56ca40bf349989537e1cb19806ddf169f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:45:21 +0100 Subject: [PATCH 081/238] Kotlin conversion: ChangeKeyPasswordExternal --- .../operation/ChangeKeyPasswordExternal.java | 62 ------------------- .../operation/ChangeKeyPasswordExternal.kt | 37 +++++++++++ 2 files changed, 37 insertions(+), 62 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java b/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java deleted file mode 100644 index d53d6a5..0000000 --- a/external-sop/src/main/java/sop/external/operation/ChangeKeyPasswordExternal.java +++ /dev/null @@ -1,62 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.ChangeKeyPassword; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class ChangeKeyPasswordExternal implements ChangeKeyPassword { - private final List commandList = new ArrayList<>(); - private final List envList; - - private int keyPasswordCounter = 0; - - public ChangeKeyPasswordExternal(String binary, Properties environment) { - this.commandList.add(binary); - this.commandList.add("decrypt"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public ChangeKeyPassword noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public ChangeKeyPassword oldKeyPassphrase(@Nonnull String oldPassphrase) { - this.commandList.add("--old-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); - this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + oldPassphrase); - keyPasswordCounter++; - - return this; - } - - @Override - @Nonnull - public ChangeKeyPassword newKeyPassphrase(@Nonnull String newPassphrase) { - this.commandList.add("--new-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); - this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + newPassphrase); - keyPasswordCounter++; - - return this; - } - - @Override - @Nonnull - public Ready keys(@Nonnull InputStream inputStream) throws SOPGPException.KeyIsProtected, SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, inputStream); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt new file mode 100644 index 0000000..a45e59f --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ChangeKeyPasswordExternal.kt @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.ChangeKeyPassword + +/** Implementation of the [ChangeKeyPassword] operation using an external SOP binary. */ +class ChangeKeyPasswordExternal(binary: String, environment: Properties) : ChangeKeyPassword { + + private val commandList: MutableList = mutableListOf(binary, "change-key-password") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var keyPasswordCounter = 0 + + override fun noArmor(): ChangeKeyPassword = apply { commandList.add("--no-armor") } + + override fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword = apply { + commandList.add("--old-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") + envList.add("KEY_PASSWORD_$keyPasswordCounter=$oldPassphrase") + keyPasswordCounter += 1 + } + + override fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword = apply { + commandList.add("--new-key-password=@ENV:KEY_PASSWORD_$keyPasswordCounter") + envList.add("KEY_PASSWORD_$keyPasswordCounter=$newPassphrase") + keyPasswordCounter += 1 + } + + override fun keys(keys: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) +} From da2b299f4d6bea493585e7640faa0d1616fd4edc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 16:45:32 +0100 Subject: [PATCH 082/238] Kotlin conversion: DearmorExternal --- .../external/operation/DearmorExternal.java | 37 ------------------- .../sop/external/operation/DearmorExternal.kt | 20 ++++++++++ 2 files changed, 20 insertions(+), 37 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DearmorExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java b/external-sop/src/main/java/sop/external/operation/DearmorExternal.java deleted file mode 100644 index cd3da6f..0000000 --- a/external-sop/src/main/java/sop/external/operation/DearmorExternal.java +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Dearmor; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Dearmor} operation using an external SOP binary. - */ -public class DearmorExternal implements Dearmor { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public DearmorExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("dearmor"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt new file mode 100644 index 0000000..928d9b4 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DearmorExternal.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.Dearmor + +/** Implementation of the [Dearmor] operation using an external SOP binary. */ +class DearmorExternal(binary: String, environment: Properties) : Dearmor { + private val commandList = listOf(binary, "dearmor") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun data(data: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) +} From 03da9bbfb705c67906c9904204b7375d7a382a7c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:04:00 +0100 Subject: [PATCH 083/238] Kotlin conversion: DecryptExternal --- .../external/operation/DecryptExternal.java | 185 ------------------ .../sop/external/operation/DecryptExternal.kt | 133 +++++++++++++ 2 files changed, 133 insertions(+), 185 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DecryptExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java b/external-sop/src/main/java/sop/external/operation/DecryptExternal.java deleted file mode 100644 index a1c4016..0000000 --- a/external-sop/src/main/java/sop/external/operation/DecryptExternal.java +++ /dev/null @@ -1,185 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.DecryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Decrypt; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Decrypt} operation using an external SOP binary. - */ -public class DecryptExternal implements Decrypt { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - private int verifyWithCounter = 0; - private int withSessionKeyCounter = 0; - private int withPasswordCounter = 0; - private int keyCounter = 0; - private int withKeyPasswordCounter = 0; - - public DecryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - this.commandList.add(binary); - this.commandList.add("decrypt"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public Decrypt verifyNotBefore(@Nonnull Date timestamp) - throws SOPGPException.UnsupportedOption { - this.commandList.add("--verify-not-before=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public Decrypt verifyNotAfter(@Nonnull Date timestamp) - throws SOPGPException.UnsupportedOption { - this.commandList.add("--verify-not-after=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public Decrypt verifyWithCert(@Nonnull InputStream cert) - throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "VERIFY_WITH_" + verifyWithCounter++; - commandList.add("--verify-with=@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - return this; - } - - @Override - @Nonnull - public Decrypt withSessionKey(@Nonnull SessionKey sessionKey) - throws SOPGPException.UnsupportedOption { - String envVar = "SESSION_KEY_" + withSessionKeyCounter++; - commandList.add("--with-session-key=@ENV:" + envVar); - envList.add(envVar + "=" + sessionKey); - return this; - } - - @Override - @Nonnull - public Decrypt withPassword(@Nonnull String password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - String envVar = "PASSWORD_" + withPasswordCounter++; - commandList.add("--with-password=@ENV:" + envVar); - envList.add(envVar + "=" + password); - return this; - } - - @Override - @Nonnull - public Decrypt withKey(@Nonnull InputStream key) - throws SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "KEY_" + keyCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public Decrypt withKeyPassword(@Nonnull byte[] password) - throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public ReadyWithResult ciphertext(@Nonnull InputStream ciphertext) - throws SOPGPException.BadData, SOPGPException.MissingArg, SOPGPException.CannotDecrypt, - SOPGPException.KeyIsProtected, IOException { - File tempDir = tempDirProvider.provideTempDirectory(); - - File sessionKeyOut = new File(tempDir, "session-key-out"); - sessionKeyOut.delete(); - commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath()); - - File verifyOut = new File(tempDir, "verifications-out"); - verifyOut.delete(); - if (verifyWithCounter != 0) { - commandList.add("--verify-out=" + verifyOut.getAbsolutePath()); - } - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = ciphertext.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - ciphertext.close(); - processOut.close(); - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut); - String line = ExternalSOP.readString(sessionKeyOutIn); - SessionKey sessionKey = SessionKey.fromString(line.trim()); - sessionKeyOutIn.close(); - sessionKeyOut.delete(); - - List verifications = new ArrayList<>(); - if (verifyWithCounter != 0) { - FileInputStream verifyOutIn = new FileInputStream(verifyOut); - BufferedReader reader = new BufferedReader(new InputStreamReader(verifyOutIn)); - while ((line = reader.readLine()) != null) { - verifications.add(Verification.fromString(line.trim())); - } - reader.close(); - } - - return new DecryptionResult(sessionKey, verifications); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt new file mode 100644 index 0000000..b68d3a6 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -0,0 +1,133 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.DecryptionResult +import sop.ReadyWithResult +import sop.SessionKey +import sop.Verification +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.external.ExternalSOP.Companion.readString +import sop.operation.Decrypt +import sop.util.UTCUtil + +/** Implementation of the [Decrypt] operation using an external SOP binary. */ +class DecryptExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : Decrypt { + + private val commandList = mutableListOf(binary, "decrypt") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + private var requireVerification = false + + override fun verifyNotBefore(timestamp: Date): Decrypt = apply { + commandList.add("--verify-not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun verifyNotAfter(timestamp: Date): Decrypt = apply { + commandList.add("--verify-not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun verifyWithCert(cert: InputStream): Decrypt = apply { + commandList.add("--verify-with=@ENV:VERIFY_WITH_$argCounter") + envList.add("VERIFY_WITH_$argCounter=${readString(cert)}") + argCounter += 1 + requireVerification = true + } + + override fun withSessionKey(sessionKey: SessionKey): Decrypt = apply { + commandList.add("--with-session-key=@ENV:SESSION_KEY_$argCounter") + envList.add("SESSION_KEY_$argCounter=$sessionKey") + argCounter += 1 + } + + override fun withPassword(password: String): Decrypt = apply { + commandList.add("--with-password=@ENV:PASSWORD_$argCounter") + envList.add("PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun withKey(key: InputStream): Decrypt = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): Decrypt = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } + + override fun ciphertext(ciphertext: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val sessionKeyOut = File(tempDir, "session-key-out") + sessionKeyOut.delete() + commandList.add("--session-key-out=${sessionKeyOut.absolutePath}") + + val verifyOut = File(tempDir, "verifications-out") + verifyOut.delete() + if (requireVerification) { + commandList.add("--verify-out=${verifyOut.absolutePath}") + } + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): DecryptionResult { + val buf = ByteArray(4096) + var r: Int + while (ciphertext.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + ciphertext.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val sessionKeyOutIn = FileInputStream(sessionKeyOut) + var line = readString(sessionKeyOutIn) + val sessionKey = SessionKey.fromString(line.trim { it <= ' ' }) + sessionKeyOutIn.close() + sessionKeyOut.delete() + + val verifications: MutableList = ArrayList() + if (requireVerification) { + val verifyOutIn = FileInputStream(verifyOut) + val reader = BufferedReader(InputStreamReader(verifyOutIn)) + while (reader.readLine().also { line = it } != null) { + verifications.add(Verification.fromString(line.trim())) + } + reader.close() + } + + return DecryptionResult(sessionKey, verifications) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 9cd9f151c9b6138b3e2fe5999efea347922ee9ae Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:13:28 +0100 Subject: [PATCH 084/238] Kotlin conversion: DetachedSignExternal --- .../operation/DetachedSignExternal.java | 142 ------------------ .../operation/DetachedSignExternal.kt | 104 +++++++++++++ 2 files changed, 104 insertions(+), 142 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java deleted file mode 100644 index 2ef2714..0000000 --- a/external-sop/src/main/java/sop/external/operation/DetachedSignExternal.java +++ /dev/null @@ -1,142 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.MicAlg; -import sop.ReadyWithResult; -import sop.SigningResult; -import sop.enums.SignAs; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.DetachedSign; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link DetachedSign} operation using an external SOP binary. - */ -public class DetachedSignExternal implements DetachedSign { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - private int withKeyPasswordCounter = 0; - private int keyCounter = 0; - - public DetachedSignExternal(String binary, Properties properties, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - commandList.add(binary); - commandList.add("sign"); - envList = ExternalSOP.propertiesToEnv(properties); - } - - @Override - @Nonnull - public DetachedSign noArmor() { - commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public DetachedSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "KEY_" + keyCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public DetachedSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public DetachedSign mode(@Nonnull SignAs mode) throws SOPGPException.UnsupportedOption { - commandList.add("--as=" + mode); - return this; - } - - @Override - @Nonnull - public ReadyWithResult data(@Nonnull InputStream data) - throws IOException, SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { - - File tempDir = tempDirProvider.provideTempDirectory(); - File micAlgOut = new File(tempDir, "micAlgOut"); - micAlgOut.delete(); - commandList.add("--micalg-out=" + micAlgOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public SigningResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = data.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - data.close(); - try { - processOut.close(); - } catch (IOException e) { - // Ignore Stream closed - if (!"Stream closed".equals(e.getMessage())) { - throw e; - } - } - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - SigningResult.Builder builder = SigningResult.builder(); - if (micAlgOut.exists()) { - BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(micAlgOut))); - String line = reader.readLine(); - if (line != null && !line.trim().isEmpty()) { - MicAlg micAlg = new MicAlg(line.trim()); - builder.setMicAlg(micAlg); - } - reader.close(); - micAlgOut.delete(); - } - - return builder.build(); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt new file mode 100644 index 0000000..66d1db8 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DetachedSignExternal.kt @@ -0,0 +1,104 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.MicAlg +import sop.ReadyWithResult +import sop.SigningResult +import sop.SigningResult.Companion.builder +import sop.enums.SignAs +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.DetachedSign + +/** Implementation of the [DetachedSign] operation using an external SOP binary. */ +class DetachedSignExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : DetachedSign { + + private val commandList = mutableListOf(binary, "sign") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun mode(mode: SignAs): DetachedSign = apply { commandList.add("--as=$mode") } + + override fun data(data: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + val micAlgOut = File(tempDir, "micAlgOut") + micAlgOut.delete() + commandList.add("--micalg-out=${micAlgOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): SigningResult { + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + try { + processOut.close() + } catch (e: IOException) { + // Ignore Stream closed + if ("Stream closed" != e.message) { + throw e + } + } + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val builder = builder() + if (micAlgOut.exists()) { + val reader = BufferedReader(InputStreamReader(FileInputStream(micAlgOut))) + val line = reader.readLine() + if (line != null && line.isNotBlank()) { + val micAlg = MicAlg(line.trim()) + builder.setMicAlg(micAlg) + } + reader.close() + micAlgOut.delete() + } + + return builder.build() + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun noArmor(): DetachedSign = apply { commandList.add("--no-armor") } + + override fun key(key: InputStream): DetachedSign = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): DetachedSign = apply { + commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter") + envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } +} From 4a405f6d394d95a5b343b5e42fc00334d0fceef3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:22:07 +0100 Subject: [PATCH 085/238] Kotlin conversion: DetachedVerifyExternal --- .../operation/DetachedVerifyExternal.java | 117 ------------------ .../operation/DetachedVerifyExternal.kt | 90 ++++++++++++++ 2 files changed, 90 insertions(+), 117 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java deleted file mode 100644 index 866150d..0000000 --- a/external-sop/src/main/java/sop/external/operation/DetachedVerifyExternal.java +++ /dev/null @@ -1,117 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Verification; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.DetachedVerify; -import sop.operation.VerifySignatures; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashSet; -import java.util.List; -import java.util.Properties; -import java.util.Set; - -/** - * Implementation of the {@link DetachedVerify} operation using an external SOP binary. - */ -public class DetachedVerifyExternal implements DetachedVerify { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private final Set certs = new HashSet<>(); - private InputStream signatures; - private int certCounter = 0; - - public DetachedVerifyExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("verify"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public DetachedVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public DetachedVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public DetachedVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData { - this.certs.add(cert); - return this; - } - - @Override - @Nonnull - public VerifySignatures signatures(@Nonnull InputStream signatures) throws SOPGPException.BadData { - this.signatures = signatures; - return this; - } - - @Override - @Nonnull - public List data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - commandList.add("@ENV:SIGNATURE"); - envList.add("SIGNATURE=" + ExternalSOP.readString(signatures)); - - for (InputStream cert : certs) { - String envVar = "CERT_" + certCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - } - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - byte[] buf = new byte[4096]; - int r; - while ((r = data.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - data.close(); - processOut.close(); - - BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(processIn)); - List verifications = new ArrayList<>(); - - String line = null; - while ((line = bufferedReader.readLine()) != null) { - verifications.add(Verification.fromString(line)); - } - - ExternalSOP.finish(process); - - return verifications; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt new file mode 100644 index 0000000..3340a33 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/DetachedVerifyExternal.kt @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader +import java.util.* +import sop.Verification +import sop.Verification.Companion.fromString +import sop.exception.SOPGPException +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.DetachedVerify +import sop.operation.VerifySignatures +import sop.util.UTCUtil + +/** Implementation of the [DetachedVerify] operation using an external SOP binary. */ +class DetachedVerifyExternal(binary: String, environment: Properties) : DetachedVerify { + + private val commandList = mutableListOf(binary, "verify") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var signatures: InputStream? = null + private val certs: MutableSet = mutableSetOf() + private var argCounter = 0 + + override fun signatures(signatures: InputStream): VerifySignatures = apply { + this.signatures = signatures + } + + override fun notBefore(timestamp: Date): DetachedVerify = apply { + commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun notAfter(timestamp: Date): DetachedVerify = apply { + commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun cert(cert: InputStream): DetachedVerify = apply { this.certs.add(cert) } + + override fun data(data: InputStream): List { + // Signature + if (signatures == null) { + throw SOPGPException.MissingArg("Missing argument: signatures cannot be null.") + } + commandList.add("@ENV:SIGNATURE") + envList.add("SIGNATURE=${ExternalSOP.readString(signatures!!)}") + + // Certs + for (cert in certs) { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") + argCounter += 1 + } + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + processOut.close() + + val bufferedReader = BufferedReader(InputStreamReader(processIn)) + val verifications: MutableList = ArrayList() + + var line: String? + while (bufferedReader.readLine().also { line = it } != null) { + verifications.add(fromString(line!!)) + } + + finish(process) + + return verifications + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From c53c69f3acb503f01bcf75d2f0601ba51c4c243a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:32:43 +0100 Subject: [PATCH 086/238] Kotlin conversion: EncryptExternal --- .../external/operation/EncryptExternal.java | 160 ------------------ .../sop/external/operation/EncryptExternal.kt | 111 ++++++++++++ 2 files changed, 111 insertions(+), 160 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/EncryptExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java b/external-sop/src/main/java/sop/external/operation/EncryptExternal.java deleted file mode 100644 index f41a36e..0000000 --- a/external-sop/src/main/java/sop/external/operation/EncryptExternal.java +++ /dev/null @@ -1,160 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.EncryptionResult; -import sop.ReadyWithResult; -import sop.SessionKey; -import sop.enums.EncryptAs; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.Encrypt; - -import javax.annotation.Nonnull; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link Encrypt} operation using an external SOP binary. - */ -public class EncryptExternal implements Encrypt { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - private int SIGN_WITH_COUNTER = 0; - private int KEY_PASSWORD_COUNTER = 0; - private int PASSWORD_COUNTER = 0; - private int CERT_COUNTER = 0; - - public EncryptExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - this.commandList.add(binary); - this.commandList.add("encrypt"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public Encrypt noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public Encrypt mode(@Nonnull EncryptAs mode) - throws SOPGPException.UnsupportedOption { - this.commandList.add("--as=" + mode); - return this; - } - - @Override - @Nonnull - public Encrypt signWith(@Nonnull InputStream key) - throws SOPGPException.KeyCannotSign, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, - IOException { - String envVar = "SIGN_WITH_" + SIGN_WITH_COUNTER++; - commandList.add("--sign-with=@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public Encrypt withKeyPassword(@Nonnull byte[] password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - String envVar = "KEY_PASSWORD_" + KEY_PASSWORD_COUNTER++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public Encrypt withPassword(@Nonnull String password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - String envVar = "PASSWORD_" + PASSWORD_COUNTER++; - commandList.add("--with-password=@ENV:" + envVar); - envList.add(envVar + "=" + password); - return this; - } - - @Override - @Nonnull - public Encrypt withCert(@Nonnull InputStream cert) - throws SOPGPException.CertCannotEncrypt, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, - IOException { - String envVar = "CERT_" + CERT_COUNTER++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - return this; - } - - @Override - @Nonnull - public Encrypt profile(@Nonnull String profileName) { - commandList.add("--profile=" + profileName); - return this; - } - - @Override - @Nonnull - public ReadyWithResult plaintext(@Nonnull InputStream plaintext) - throws SOPGPException.KeyIsProtected, IOException { - File tempDir = tempDirProvider.provideTempDirectory(); - - File sessionKeyOut = new File(tempDir, "session-key-out"); - sessionKeyOut.delete(); - commandList.add("--session-key-out=" + sessionKeyOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public EncryptionResult writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = plaintext.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - plaintext.close(); - processOut.close(); - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream sessionKeyOutIn = new FileInputStream(sessionKeyOut); - String line = ExternalSOP.readString(sessionKeyOutIn); - SessionKey sessionKey = SessionKey.fromString(line.trim()); - sessionKeyOutIn.close(); - sessionKeyOut.delete(); - - return new EncryptionResult(sessionKey); - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt new file mode 100644 index 0000000..6f1cc6c --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.* +import sop.EncryptionResult +import sop.ReadyWithResult +import sop.SessionKey.Companion.fromString +import sop.enums.EncryptAs +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.external.ExternalSOP.Companion.readString +import sop.operation.Encrypt + +/** Implementation of the [Encrypt] operation using an external SOP binary. */ +class EncryptExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : Encrypt { + + private val commandList = mutableListOf(binary, "encrypt") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun noArmor(): Encrypt = apply { commandList.add("--no-armor") } + + override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") } + + override fun signWith(key: InputStream): Encrypt = apply { + commandList.add("--sign-with@ENV:SIGN_WITH_$argCounter") + envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): Encrypt = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } + + override fun withPassword(password: String): Encrypt = apply { + commandList.add("--with-password=@ENV:PASSWORD_$argCounter") + envList.add("PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun withCert(cert: InputStream): Encrypt = apply { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${readString(cert)}") + argCounter += 1 + } + + override fun profile(profileName: String): Encrypt = apply { + commandList.add("--profile=$profileName") + } + + override fun plaintext(plaintext: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val sessionKeyOut = File(tempDir, "session-key-out") + sessionKeyOut.delete() + commandList.add("--session-key-out=${sessionKeyOut.absolutePath}") + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): EncryptionResult { + val buf = ByteArray(4096) + var r: Int + while (plaintext.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + plaintext.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val sessionKeyOutIn = FileInputStream(sessionKeyOut) + val line = readString(sessionKeyOutIn) + val sessionKey = fromString(line.trim()) + sessionKeyOutIn.close() + sessionKeyOut.delete() + + return EncryptionResult(sessionKey) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 01abae4d080d47660d4c881ef2fe6bf2bdc26bd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:36:00 +0100 Subject: [PATCH 087/238] Kotlin conversion: ExtractCertExternal --- .../operation/ExtractCertExternal.java | 44 ------------------- .../external/operation/ExtractCertExternal.kt | 24 ++++++++++ 2 files changed, 24 insertions(+), 44 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java b/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java deleted file mode 100644 index 538359a..0000000 --- a/external-sop/src/main/java/sop/external/operation/ExtractCertExternal.java +++ /dev/null @@ -1,44 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.ExtractCert; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link ExtractCert} operation using an external SOP binary. - */ -public class ExtractCertExternal implements ExtractCert { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public ExtractCertExternal(String binary, Properties properties) { - this.commandList.add(binary); - this.commandList.add("extract-cert"); - this.envList = ExternalSOP.propertiesToEnv(properties); - } - - @Override - @Nonnull - public ExtractCert noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public Ready key(@Nonnull InputStream keyInputStream) throws SOPGPException.BadData { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keyInputStream); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt new file mode 100644 index 0000000..9b86733 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ExtractCertExternal.kt @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.ExtractCert + +/** Implementation of the [ExtractCert] operation using an external SOP binary. */ +class ExtractCertExternal(binary: String, environment: Properties) : ExtractCert { + + private val commandList = mutableListOf(binary, "extract-cert") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun noArmor(): ExtractCert = apply { commandList.add("--no-armor") } + + override fun key(keyInputStream: InputStream): Ready = + ExternalSOP.executeTransformingOperation( + Runtime.getRuntime(), commandList, envList, keyInputStream) +} From 9b79a49bb54a7a5d5ef588394f9f63dc20e463e1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:41:33 +0100 Subject: [PATCH 088/238] Kotlin conversion: GenerateKeyExternal --- .../operation/GenerateKeyExternal.java | 78 ------------------- .../external/operation/GenerateKeyExternal.kt | 38 +++++++++ 2 files changed, 38 insertions(+), 78 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java b/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java deleted file mode 100644 index a8244cb..0000000 --- a/external-sop/src/main/java/sop/external/operation/GenerateKeyExternal.java +++ /dev/null @@ -1,78 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.GenerateKey; - -import javax.annotation.Nonnull; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link GenerateKey} operation using an external SOP binary. - */ -public class GenerateKeyExternal implements GenerateKey { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private int keyPasswordCounter = 0; - - public GenerateKeyExternal(String binary, Properties environment) { - this.commandList.add(binary); - this.commandList.add("generate-key"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public GenerateKey noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public GenerateKey userId(@Nonnull String userId) { - this.commandList.add(userId); - return this; - } - - @Override - @Nonnull - public GenerateKey withKeyPassword(@Nonnull String password) - throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - this.commandList.add("--with-key-password=@ENV:KEY_PASSWORD_" + keyPasswordCounter); - this.envList.add("KEY_PASSWORD_" + keyPasswordCounter + "=" + password); - keyPasswordCounter++; - - return this; - } - - @Override - @Nonnull - public GenerateKey profile(@Nonnull String profile) { - commandList.add("--profile=" + profile); - return this; - } - - @Override - @Nonnull - public GenerateKey signingOnly() { - commandList.add("--signing-only"); - return this; - } - - @Override - @Nonnull - public Ready generate() - throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { - return ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt new file mode 100644 index 0000000..37116a4 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/GenerateKeyExternal.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.GenerateKey + +/** Implementation of the [GenerateKey] operation using an external SOP binary. */ +class GenerateKeyExternal(binary: String, environment: Properties) : GenerateKey { + + private val commandList = mutableListOf(binary, "generate-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun noArmor(): GenerateKey = apply { commandList.add("--no-armor") } + + override fun userId(userId: String): GenerateKey = apply { commandList.add(userId) } + + override fun withKeyPassword(password: String): GenerateKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCounter") + envList.add("KEY_PASSWORD_$argCounter=$password") + argCounter += 1 + } + + override fun profile(profile: String): GenerateKey = apply { + commandList.add("--profile=$profile") + } + + override fun signingOnly(): GenerateKey = apply { commandList.add("--signing-only") } + + override fun generate(): Ready = + ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList) +} From f1814530041462e65cb137dda085893d03a6a889 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:47:37 +0100 Subject: [PATCH 089/238] Kotlin conversion: InlineDetachExternal --- .../operation/InlineDetachExternal.java | 106 ------------------ .../operation/InlineDetachExternal.kt | 82 ++++++++++++++ 2 files changed, 82 insertions(+), 106 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java b/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java deleted file mode 100644 index a798006..0000000 --- a/external-sop/src/main/java/sop/external/operation/InlineDetachExternal.java +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.ReadyWithResult; -import sop.Signatures; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.InlineDetach; - -import javax.annotation.Nonnull; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link InlineDetach} operation using an external SOP binary. - */ -public class InlineDetachExternal implements InlineDetach { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - public InlineDetachExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - commandList.add(binary); - commandList.add("inline-detach"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public InlineDetach noArmor() { - commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public ReadyWithResult message(@Nonnull InputStream messageInputStream) throws IOException, SOPGPException.BadData { - File tempDir = tempDirProvider.provideTempDirectory(); - - File signaturesOut = new File(tempDir, "signatures"); - signaturesOut.delete(); - commandList.add("--signatures-out=" + signaturesOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult() { - @Override - public Signatures writeTo(@Nonnull OutputStream outputStream) throws IOException { - byte[] buf = new byte[4096]; - int r; - while ((r = messageInputStream.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - messageInputStream.close(); - processOut.close(); - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream signaturesOutIn = new FileInputStream(signaturesOut); - ByteArrayOutputStream signaturesBuffer = new ByteArrayOutputStream(); - while ((r = signaturesOutIn.read(buf)) > 0) { - signaturesBuffer.write(buf, 0, r); - } - signaturesOutIn.close(); - signaturesOut.delete(); - - final byte[] sigBytes = signaturesBuffer.toByteArray(); - return new Signatures() { - @Override - public void writeTo(@Nonnull OutputStream signatureOutputStream) throws IOException { - signatureOutputStream.write(sigBytes); - } - }; - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt new file mode 100644 index 0000000..f44e6bb --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineDetachExternal.kt @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.ReadyWithResult +import sop.Signatures +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.InlineDetach + +/** Implementation of the [InlineDetach] operation using an external SOP binary. */ +class InlineDetachExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : InlineDetach { + + private val commandList = mutableListOf(binary, "inline-detach") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun noArmor(): InlineDetach = apply { commandList.add("--no-armor") } + + override fun message(messageInputStream: InputStream): ReadyWithResult { + val tempDir = tempDirProvider.provideTempDirectory() + + val signaturesOut = File(tempDir, "signatures") + signaturesOut.delete() + commandList.add("--signatures-out=${signaturesOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult() { + override fun writeTo(outputStream: OutputStream): Signatures { + val buf = ByteArray(4096) + var r: Int + while (messageInputStream.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + messageInputStream.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val signaturesOutIn = FileInputStream(signaturesOut) + val signaturesBuffer = ByteArrayOutputStream() + while (signaturesOutIn.read(buf).also { r = it } > 0) { + signaturesBuffer.write(buf, 0, r) + } + signaturesOutIn.close() + signaturesOut.delete() + + val sigBytes = signaturesBuffer.toByteArray() + + return object : Signatures() { + @Throws(IOException::class) + override fun writeTo(outputStream: OutputStream) { + outputStream.write(sigBytes) + } + } + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 7be71494cfdc063d7e370c7305bd017061532e86 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:52:56 +0100 Subject: [PATCH 090/238] Kotlin conversion: InlineSignExternal --- .../operation/InlineSignExternal.java | 74 ------------------- .../external/operation/InlineSignExternal.kt | 40 ++++++++++ 2 files changed, 40 insertions(+), 74 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/InlineSignExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java b/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java deleted file mode 100644 index 1a86002..0000000 --- a/external-sop/src/main/java/sop/external/operation/InlineSignExternal.java +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.enums.InlineSignAs; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.InlineSign; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link InlineSign} operation using an external SOP binary. - */ -public class InlineSignExternal implements InlineSign { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private int keyCounter = 0; - private int withKeyPasswordCounter = 0; - - public InlineSignExternal(String binary, Properties environment) { - commandList.add(binary); - commandList.add("inline-sign"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public InlineSign noArmor() { - commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public InlineSign key(@Nonnull InputStream key) throws SOPGPException.KeyCannotSign, SOPGPException.BadData, SOPGPException.UnsupportedAsymmetricAlgo, IOException { - String envVar = "KEY_" + keyCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(key)); - return this; - } - - @Override - @Nonnull - public InlineSign withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "WITH_KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public InlineSign mode(@Nonnull InlineSignAs mode) throws SOPGPException.UnsupportedOption { - commandList.add("--as=" + mode); - return this; - } - - @Override - @Nonnull - public Ready data(@Nonnull InputStream data) throws SOPGPException.KeyIsProtected, SOPGPException.ExpectedText { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt new file mode 100644 index 0000000..a304e85 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineSignExternal.kt @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.enums.InlineSignAs +import sop.external.ExternalSOP +import sop.operation.InlineSign + +/** Implementation of the [InlineSign] operation using an external SOP binary. */ +class InlineSignExternal(binary: String, environment: Properties) : InlineSign { + + private val commandList = mutableListOf(binary, "inline-sign") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun mode(mode: InlineSignAs): InlineSign = apply { commandList.add("--as=$mode") } + + override fun data(data: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) + + override fun noArmor(): InlineSign = apply { commandList.add("--no-armor") } + + override fun key(key: InputStream): InlineSign = apply { + commandList.add("@ENV:KEY_$argCounter") + envList.add("KEY_$argCounter=${ExternalSOP.readString(key)}") + argCounter += 1 + } + + override fun withKeyPassword(password: ByteArray): InlineSign = apply { + commandList.add("--with-key-password=@ENV:WITH_KEY_PASSWORD_$argCounter") + envList.add("WITH_KEY_PASSWORD_$argCounter=${String(password)}") + argCounter += 1 + } +} From 8dc51b67a3e58a768000f38967eafeef4c05a913 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 17:59:04 +0100 Subject: [PATCH 091/238] Kotlin conversion: InlineVerifyExternal --- .../operation/InlineVerifyExternal.java | 122 ------------------ .../operation/InlineVerifyExternal.kt | 91 +++++++++++++ 2 files changed, 91 insertions(+), 122 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java b/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java deleted file mode 100644 index 10a2d47..0000000 --- a/external-sop/src/main/java/sop/external/operation/InlineVerifyExternal.java +++ /dev/null @@ -1,122 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.ReadyWithResult; -import sop.Verification; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.InlineVerify; -import sop.util.UTCUtil; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Properties; - -/** - * Implementation of the {@link InlineVerify} operation using an external SOP binary. - */ -public class InlineVerifyExternal implements InlineVerify { - - private final ExternalSOP.TempDirProvider tempDirProvider; - private final List commandList = new ArrayList<>(); - private final List envList; - - private int certCounter = 0; - - public InlineVerifyExternal(String binary, Properties environment, ExternalSOP.TempDirProvider tempDirProvider) { - this.tempDirProvider = tempDirProvider; - commandList.add(binary); - commandList.add("inline-verify"); - envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public InlineVerify notBefore(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-before=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public InlineVerify notAfter(@Nonnull Date timestamp) throws SOPGPException.UnsupportedOption { - commandList.add("--not-after=" + UTCUtil.formatUTCDate(timestamp)); - return this; - } - - @Override - @Nonnull - public InlineVerify cert(@Nonnull InputStream cert) throws SOPGPException.BadData, IOException { - String envVar = "CERT_" + certCounter++; - commandList.add("@ENV:" + envVar); - envList.add(envVar + "=" + ExternalSOP.readString(cert)); - return this; - } - - @Override - @Nonnull - public ReadyWithResult> data(@Nonnull InputStream data) throws IOException, SOPGPException.NoSignature, SOPGPException.BadData { - File tempDir = tempDirProvider.provideTempDirectory(); - - File verificationsOut = new File(tempDir, "verifications-out"); - verificationsOut.delete(); - commandList.add("--verifications-out=" + verificationsOut.getAbsolutePath()); - - String[] command = commandList.toArray(new String[0]); - String[] env = envList.toArray(new String[0]); - - try { - Process process = Runtime.getRuntime().exec(command, env); - OutputStream processOut = process.getOutputStream(); - InputStream processIn = process.getInputStream(); - - return new ReadyWithResult>() { - @Override - public List writeTo(@Nonnull OutputStream outputStream) throws IOException, SOPGPException.NoSignature { - byte[] buf = new byte[4096]; - int r; - while ((r = data.read(buf)) > 0) { - processOut.write(buf, 0, r); - } - - data.close(); - processOut.close(); - - - while ((r = processIn.read(buf)) > 0) { - outputStream.write(buf, 0 , r); - } - - processIn.close(); - outputStream.close(); - - ExternalSOP.finish(process); - - FileInputStream verificationsOutIn = new FileInputStream(verificationsOut); - BufferedReader reader = new BufferedReader(new InputStreamReader(verificationsOutIn)); - List verificationList = new ArrayList<>(); - String line; - while ((line = reader.readLine()) != null) { - verificationList.add(Verification.fromString(line.trim())); - } - - return verificationList; - } - }; - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt new file mode 100644 index 0000000..ed769e3 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.* +import java.util.* +import sop.ReadyWithResult +import sop.Verification +import sop.Verification.Companion.fromString +import sop.external.ExternalSOP +import sop.external.ExternalSOP.Companion.finish +import sop.operation.InlineVerify +import sop.util.UTCUtil + +/** Implementation of the [InlineVerify] operation using an external SOP binary. */ +class InlineVerifyExternal( + binary: String, + environment: Properties, + private val tempDirProvider: ExternalSOP.TempDirProvider +) : InlineVerify { + + private val commandList = mutableListOf(binary, "inline-verify") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCounter = 0 + + override fun data(data: InputStream): ReadyWithResult> { + val tempDir = tempDirProvider.provideTempDirectory() + + val verificationsOut = File(tempDir, "verifications-out") + verificationsOut.delete() + commandList.add("--verifications-out=${verificationsOut.absolutePath}") + + try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val processOut = process.outputStream + val processIn = process.inputStream + + return object : ReadyWithResult>() { + override fun writeTo(outputStream: OutputStream): List { + val buf = ByteArray(4096) + var r: Int + while (data.read(buf).also { r = it } > 0) { + processOut.write(buf, 0, r) + } + + data.close() + processOut.close() + + while (processIn.read(buf).also { r = it } > 0) { + outputStream.write(buf, 0, r) + } + + processIn.close() + outputStream.close() + + finish(process) + + val verificationsOutIn = FileInputStream(verificationsOut) + val reader = BufferedReader(InputStreamReader(verificationsOutIn)) + val verificationList: MutableList = mutableListOf() + var line: String + while (reader.readLine().also { line = it } != null) { + verificationList.add(fromString(line.trim())) + } + + return verificationList + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + override fun notBefore(timestamp: Date): InlineVerify = apply { + commandList.add("--not-before=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun notAfter(timestamp: Date): InlineVerify = apply { + commandList.add("--not-after=${UTCUtil.formatUTCDate(timestamp)}") + } + + override fun cert(cert: InputStream): InlineVerify = apply { + commandList.add("@ENV:CERT_$argCounter") + envList.add("CERT_$argCounter=${ExternalSOP.readString(cert)}") + argCounter += 1 + } +} From f2204dfd4d091d868e9435e672187ced4c58b56e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:06:35 +0100 Subject: [PATCH 092/238] Kotlin conversion: ListProfilesExternal --- .../operation/ListProfilesExternal.java | 50 ------------------- .../operation/ListProfilesExternal.kt | 36 +++++++++++++ 2 files changed, 36 insertions(+), 50 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java b/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java deleted file mode 100644 index 21d5e13..0000000 --- a/external-sop/src/main/java/sop/external/operation/ListProfilesExternal.java +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Profile; -import sop.external.ExternalSOP; -import sop.operation.ListProfiles; - -import javax.annotation.Nonnull; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class ListProfilesExternal implements ListProfiles { - - private final List commandList = new ArrayList<>(); - private final List envList; - - public ListProfilesExternal(String binary, Properties properties) { - this.commandList.add(binary); - this.commandList.add("list-profiles"); - this.envList = ExternalSOP.propertiesToEnv(properties); - } - - @Override - @Nonnull - public List subcommand(@Nonnull String command) { - commandList.add(command); - try { - String output = new String(ExternalSOP.executeProducingOperation(Runtime.getRuntime(), commandList, envList).getBytes()); - return toProfiles(output); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static List toProfiles(String output) { - List profiles = new ArrayList<>(); - for (String line : output.split("\n")) { - if (line.trim().isEmpty()) { - continue; - } - profiles.add(Profile.parse(line)); - } - return profiles; - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt new file mode 100644 index 0000000..5e8ff89 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ListProfilesExternal.kt @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.IOException +import java.util.Properties +import sop.Profile +import sop.external.ExternalSOP +import sop.operation.ListProfiles + +/** Implementation of the [ListProfiles] operation using an external SOP binary. */ +class ListProfilesExternal(binary: String, environment: Properties) : ListProfiles { + + private val commandList = mutableListOf(binary, "list-profiles") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun subcommand(command: String): List { + return try { + String( + ExternalSOP.executeProducingOperation( + Runtime.getRuntime(), commandList.plus(command), envList) + .bytes) + .let { toProfiles(it) } + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + companion object { + @JvmStatic + private fun toProfiles(output: String): List = + output.split("\n").filter { it.isNotBlank() }.map { Profile.parse(it) } + } +} From 832a455c4ccb885d68ebb97e6b855d0e80faa24d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:11:24 +0100 Subject: [PATCH 093/238] Kotlin conversion: RevokeKeyExternal --- .../external/operation/RevokeKeyExternal.java | 52 ------------------- .../external/operation/RevokeKeyExternal.kt | 31 +++++++++++ 2 files changed, 31 insertions(+), 52 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java deleted file mode 100644 index 72ed549..0000000 --- a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.Ready; -import sop.exception.SOPGPException; -import sop.external.ExternalSOP; -import sop.operation.RevokeKey; - -import javax.annotation.Nonnull; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.List; -import java.util.Properties; - -public class RevokeKeyExternal implements RevokeKey { - - private final List commandList = new ArrayList<>(); - private final List envList; - - private int withKeyPasswordCounter = 0; - - public RevokeKeyExternal(String binary, Properties environment) { - this.commandList.add(binary); - this.commandList.add("revoke-key"); - this.envList = ExternalSOP.propertiesToEnv(environment); - } - - @Override - @Nonnull - public RevokeKey noArmor() { - this.commandList.add("--no-armor"); - return this; - } - - @Override - @Nonnull - public RevokeKey withKeyPassword(@Nonnull byte[] password) throws SOPGPException.UnsupportedOption, SOPGPException.PasswordNotHumanReadable { - String envVar = "KEY_PASSWORD_" + withKeyPasswordCounter++; - commandList.add("--with-key-password=@ENV:" + envVar); - envList.add(envVar + "=" + new String(password)); - return this; - } - - @Override - @Nonnull - public Ready keys(@Nonnull InputStream keys) { - return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys); - } -} diff --git a/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt new file mode 100644 index 0000000..43795e6 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/RevokeKeyExternal.kt @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.InputStream +import java.util.Properties +import sop.Ready +import sop.external.ExternalSOP +import sop.operation.RevokeKey + +/** Implementation of the [RevokeKey] operation using an external SOP binary. */ +class RevokeKeyExternal(binary: String, environment: Properties) : RevokeKey { + + private val commandList = mutableListOf(binary, "revoke-key") + private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() + + private var argCount = 0 + + override fun noArmor(): RevokeKey = apply { commandList.add("--no-armor") } + + override fun withKeyPassword(password: ByteArray): RevokeKey = apply { + commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") + envList.add("KEY_PASSWORD_$argCount=${String(password)}") + argCount += 1 + } + + override fun keys(keys: InputStream): Ready = + ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys) +} From 3eaae149b721f046ae22b4835782f8d49a0d8efe Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:34:30 +0100 Subject: [PATCH 094/238] Kotlin conversion: VersionExternal --- .../external/operation/VersionExternal.java | 163 ------------------ .../sop/external/operation/package-info.java | 8 - .../main/java/sop/external/package-info.java | 8 - .../sop/external/operation/VersionExternal.kt | 98 +++++++++++ 4 files changed, 98 insertions(+), 179 deletions(-) delete mode 100644 external-sop/src/main/java/sop/external/operation/VersionExternal.java delete mode 100644 external-sop/src/main/java/sop/external/operation/package-info.java delete mode 100644 external-sop/src/main/java/sop/external/package-info.java create mode 100644 external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt diff --git a/external-sop/src/main/java/sop/external/operation/VersionExternal.java b/external-sop/src/main/java/sop/external/operation/VersionExternal.java deleted file mode 100644 index ab2bbef..0000000 --- a/external-sop/src/main/java/sop/external/operation/VersionExternal.java +++ /dev/null @@ -1,163 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.external.operation; - -import sop.external.ExternalSOP; -import sop.operation.Version; - -import javax.annotation.Nonnull; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.util.Properties; - -/** - * Implementation of the {@link Version} operation using an external SOP binary. - */ -public class VersionExternal implements Version { - - private final Runtime runtime = Runtime.getRuntime(); - private final String binary; - private final Properties environment; - - public VersionExternal(String binaryName, Properties environment) { - this.binary = binaryName; - this.environment = environment; - } - - @Override - @Nonnull - public String getName() { - String[] command = new String[] {binary, "version"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - String line = stdInput.readLine().trim(); - ExternalSOP.finish(process); - if (line.contains(" ")) { - return line.substring(0, line.lastIndexOf(" ")); - } - return line; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - @Nonnull - public String getVersion() { - String[] command = new String[] {binary, "version"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - String line = stdInput.readLine().trim(); - ExternalSOP.finish(process); - if (line.contains(" ")) { - return line.substring(line.lastIndexOf(" ") + 1); - } - return line; - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - @Nonnull - public String getBackendVersion() { - String[] command = new String[] {binary, "version", "--backend"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = stdInput.readLine()) != null) { - sb.append(line).append('\n'); - } - ExternalSOP.finish(process); - return sb.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - @Nonnull - public String getExtendedVersion() { - String[] command = new String[] {binary, "version", "--extended"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = stdInput.readLine()) != null) { - sb.append(line).append('\n'); - } - ExternalSOP.finish(process); - return sb.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - - @Override - public int getSopSpecRevisionNumber() { - String revision = getSopSpecVersion(); - String firstLine; - if (revision.contains("\n")) { - firstLine = revision.substring(0, revision.indexOf("\n")); - } else { - firstLine = revision; - } - - if (!firstLine.contains("-")) { - return -1; - } - - return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1)); - } - - @Override - public boolean isSopSpecImplementationIncomplete() { - String revision = getSopSpecVersion(); - return revision.startsWith("~"); - } - - @Override - public String getSopSpecImplementationRemarks() { - String revision = getSopSpecVersion(); - if (revision.contains("\n")) { - String tail = revision.substring(revision.indexOf("\n") + 1).trim(); - - if (!tail.isEmpty()) { - return tail; - } - } - return null; - } - - @Override - @Nonnull - public String getSopSpecVersion() { - String[] command = new String[] {binary, "version", "--sop-spec"}; - String[] env = ExternalSOP.propertiesToEnv(environment).toArray(new String[0]); - try { - Process process = runtime.exec(command, env); - BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); - StringBuilder sb = new StringBuilder(); - String line; - while ((line = stdInput.readLine()) != null) { - sb.append(line).append('\n'); - } - ExternalSOP.finish(process); - return sb.toString(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/external-sop/src/main/java/sop/external/operation/package-info.java b/external-sop/src/main/java/sop/external/operation/package-info.java deleted file mode 100644 index 9c7dd29..0000000 --- a/external-sop/src/main/java/sop/external/operation/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Bindings for SOP subcommands to a SOP binary. - */ -package sop.external.operation; diff --git a/external-sop/src/main/java/sop/external/package-info.java b/external-sop/src/main/java/sop/external/package-info.java deleted file mode 100644 index 208985e..0000000 --- a/external-sop/src/main/java/sop/external/package-info.java +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -/** - * Implementation of sop-java which delegates execution to a binary implementing the SOP command line interface. - */ -package sop.external; diff --git a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt new file mode 100644 index 0000000..7e13fc1 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.external.operation + +import java.io.IOException +import java.util.Properties +import sop.external.ExternalSOP +import sop.operation.Version + +/** Implementation of the [Version] operation using an external SOP binary. */ +class VersionExternal(binary: String, environment: Properties) : Version { + + private val commandList = listOf(binary, "version") + private val envList = ExternalSOP.propertiesToEnv(environment) + + override fun getName(): String { + val info = executeForLine(commandList) + return if (info.contains(" ")) { + info.substring(0, info.lastIndexOf(" ")) + } else { + info + } + } + + override fun getVersion(): String { + val info = executeForLine(commandList) + return if (info.contains(" ")) { + info.substring(info.lastIndexOf(" ") + 1) + } else { + info + } + } + + override fun getBackendVersion(): String { + return executeForLines(commandList.plus("--backend")) + } + + override fun getExtendedVersion(): String { + return executeForLines(commandList.plus("--extended")) + } + + override fun getSopSpecRevisionNumber(): Int { + val revision = getSopSpecVersion() + val firstLine = + if (revision.contains("\n")) { + revision.substring(0, revision.indexOf("\n")) + } else { + revision + } + + if (!firstLine.contains("-")) { + return -1 + } + return Integer.parseInt(firstLine.substring(firstLine.lastIndexOf("-") + 1)) + } + + override fun isSopSpecImplementationIncomplete(): Boolean { + return getSopSpecVersion().startsWith("~") + } + + override fun getSopSpecImplementationRemarks(): String? { + val revision = getSopSpecVersion() + if (revision.contains("\n")) { + revision.substring(revision.indexOf("\n")).trim().takeIf { it.isNotBlank() } + } + return null + } + + override fun getSopSpecVersion(): String { + return executeForLines(commandList.plus("--sop-spec")) + } + + private fun executeForLine(commandList: List): String { + return try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val result = process.inputStream.bufferedReader().readLine() + ExternalSOP.finish(process) + result.trim() + } catch (e: IOException) { + throw RuntimeException(e) + } + } + + private fun executeForLines(commandList: List): String { + return try { + val process = + Runtime.getRuntime().exec(commandList.toTypedArray(), envList.toTypedArray()) + val result = process.inputStream.bufferedReader().readLines().joinToString("\n") + ExternalSOP.finish(process) + result.trim() + } catch (e: IOException) { + throw RuntimeException(e) + } + } +} From 6c952efca23ae932e9d8e6e691aa555f50953eb4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:50:34 +0100 Subject: [PATCH 095/238] Fix NPE in line iterator --- .../kotlin/sop/external/operation/InlineVerifyExternal.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt index ed769e3..bf0c66b 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/InlineVerifyExternal.kt @@ -62,9 +62,9 @@ class InlineVerifyExternal( val verificationsOutIn = FileInputStream(verificationsOut) val reader = BufferedReader(InputStreamReader(verificationsOutIn)) val verificationList: MutableList = mutableListOf() - var line: String + var line: String? while (reader.readLine().also { line = it } != null) { - verificationList.add(fromString(line.trim())) + verificationList.add(fromString(line!!.trim())) } return verificationList From 60758dfa2f71817530a4371a0c244456095d3b27 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:58:14 +0100 Subject: [PATCH 096/238] Update changelog --- CHANGELOG.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d736ded..f168283 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,12 @@ SPDX-License-Identifier: Apache-2.0 # Changelog ## 8.0.0-SNAPSHOT -- Rewrote API in Kotlin +- Rewrote `sop-java` in Kotlin +- Rewrote `sop-java-picocli` in Kotlin +- Rewrote `external-sop` in Kotlin - Update implementation to [SOP Specification revision 08](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-08.html). - Add `--no-armor` option to `revoke-key` and `change-key-password` subcommands - - `armor`: Deprecate `--label` option + - `armor`: Deprecate `--label` option in `sop-java` and remove in `sop-java-picocli` - `encrypt`: Add `--session-key-out` option - Slight API changes: - `sop.encrypt().plaintext()` now returns a `ReadyWithResult` instead of `Ready`. From 7eeb159f1248347bf2cdff36322cf03f7ec93894 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 18:59:50 +0100 Subject: [PATCH 097/238] SOP-Java 8.0.0 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f168283..4b5d562 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 8.0.0-SNAPSHOT +## 8.0.0 - Rewrote `sop-java` in Kotlin - Rewrote `sop-java-picocli` in Kotlin - Rewrote `external-sop` in Kotlin diff --git a/version.gradle b/version.gradle index c5a9cb1..a7c71aa 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '8.0.0' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From ae83ddcff6adc598b3f17a7d2bcbf34279fc8619 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 15 Nov 2023 19:01:50 +0100 Subject: [PATCH 098/238] SOP-Java 8.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index a7c71aa..b1d7770 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '8.0.0' - isSnapshot = false + shortVersion = '8.0.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 51d9c298379faebad4c63655ecfe9071b163a753 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 17:11:24 +0100 Subject: [PATCH 099/238] decrypt --verify-with: Do not expect exit 3 when verifications is empty --- .../sop/cli/picocli/commands/DecryptCmdTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index c2c67ba..62070c2 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -21,6 +21,7 @@ import sop.operation.Decrypt; import sop.util.HexUtil; import sop.util.UTCUtil; +import javax.annotation.Nonnull; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.File; @@ -247,15 +248,17 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE) - public void assertNoSignatureExceptionCausesExit3() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { + public void assertNoVerificationsIsOkay() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { + File tempFile = File.createTempFile("verify-with-", ".tmp"); + File verifyOut = new File(tempFile.getParent(), "verifications.out"); + verifyOut.deleteOnExit(); when(decrypt.ciphertext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override - public DecryptionResult writeTo(OutputStream outputStream) throws SOPGPException.NoSignature { - throw new SOPGPException.NoSignature(); + public DecryptionResult writeTo(@Nonnull OutputStream outputStream) throws SOPGPException.NoSignature { + return new DecryptionResult(null, Collections.emptyList()); } }); - SopCLI.main(new String[] {"decrypt"}); + SopCLI.main(new String[] {"decrypt", "--verify-with", tempFile.getAbsolutePath(), "--verifications-out", verifyOut.getAbsolutePath()}); } @Test From e5e64003f3840726cbf92021e0019757a4ecfaff Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 17:11:48 +0100 Subject: [PATCH 100/238] decrypt: Do not throw NoSignature exception when verifications is empty --- .../kotlin/sop/cli/picocli/commands/DecryptCmd.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt index 1e688b3..3f15f07 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -85,13 +85,10 @@ class DecryptCmd : AbstractSopCmd() { @Throws(IOException::class) private fun writeVerifyOut(result: DecryptionResult) { verifyOut?.let { - if (result.verifications.isEmpty()) { - val errorMsg = getMsg("sop.error.runtime.no_verifiable_signature_found") - throw NoSignature(errorMsg) - } - - getOutput(verifyOut).use { out -> - PrintWriter(out).use { pw -> result.verifications.forEach { pw.println(it) } } + getOutput(it).use { out -> + PrintWriter(out).use { pw -> + result.verifications.forEach { verification -> pw.println(verification) } + } } } } From 592aecd64605deabd7cb7b9d27167b35e79f6a31 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 18:19:13 +0100 Subject: [PATCH 101/238] SOP-Java 8.0.1 --- CHANGELOG.md | 3 +++ version.gradle | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5d562..f188b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 8.0.1 +- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. + ## 8.0.0 - Rewrote `sop-java` in Kotlin - Rewrote `sop-java-picocli` in Kotlin diff --git a/version.gradle b/version.gradle index b1d7770..74bcad6 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '8.0.1' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 7092baee4f664941c27304312b653899d5cd27da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 22 Nov 2023 18:21:41 +0100 Subject: [PATCH 102/238] SOP-Java 8.0.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 74bcad6..434fdc5 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '8.0.1' - isSnapshot = false + shortVersion = '8.0.2' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From e2a568e73e490d2c96f0f84e99b348b40e456fcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 26 Feb 2024 11:03:16 +0100 Subject: [PATCH 103/238] Update issue templates --- .github/ISSUE_TEMPLATE/bug_report.md | 29 ++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..4f92d0e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** + + +**Version** + +- `sop-java`: +- `pgpainless-core`: +- `bouncycastle`: + +**To Reproduce** + +``` +Example Code Block +``` + +**Expected behavior** + + +**Additional context** + From d5d7d67d6f400430d89693ca4066d60742e434ab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 17:58:04 +0100 Subject: [PATCH 104/238] Fix reuse compliance --- .reuse/dep5 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.reuse/dep5 b/.reuse/dep5 index 8feb5a2..f43556c 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -22,3 +22,8 @@ License: Apache-2.0 Files: external-sop/src/main/resources/sop/testsuite/external/* Copyright: 2023 the original author or authors License: Apache-2.0 + +# Github Issue Templates +Files: .github/ISSUE_TEMPLATE/* +Copyright: 2024 the original author or authors +License: Apache-2.0 From 03f8950b16c72aa5b5ab771a6b21e8e0e94f19eb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 18:05:35 +0100 Subject: [PATCH 105/238] Rename woodpecker files --- .woodpecker/{.build.yml => build.yml} | 0 .woodpecker/{.reuse.yml => reuse.yml} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename .woodpecker/{.build.yml => build.yml} (100%) rename .woodpecker/{.reuse.yml => reuse.yml} (100%) diff --git a/.woodpecker/.build.yml b/.woodpecker/build.yml similarity index 100% rename from .woodpecker/.build.yml rename to .woodpecker/build.yml diff --git a/.woodpecker/.reuse.yml b/.woodpecker/reuse.yml similarity index 100% rename from .woodpecker/.reuse.yml rename to .woodpecker/reuse.yml From 173bc55eb9a6110894d845e5eba8ef9ce5784c3e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:41:44 +0100 Subject: [PATCH 106/238] Fix javadoc reference --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 3a0ef52..27c93ae 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -16,7 +16,7 @@ import sop.external.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 * empty environment variables. From a0e7356757780e96cd42a337d46011787f71eb7b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:42:54 +0100 Subject: [PATCH 107/238] Replace assumeTrue(false) with explicit TestAbortedException --- .../java/sop/testsuite/operation/VersionTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 73ba571..0b19d20 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.opentest4j.TestAbortedException; import sop.SOP; import java.util.stream.Stream; @@ -15,7 +16,6 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class VersionTest extends AbstractSOPTest { @@ -59,7 +59,7 @@ public class VersionTest extends AbstractSOPTest { try { sop.version().getSopSpecVersion(); } 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(); From 7b04275625de94b084da6b75102fb7b7141b9795 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:43:39 +0100 Subject: [PATCH 108/238] Add test ckecking that BadData is thrown if KEYS is passed for CERTS --- .../sop/testsuite/operation/EncryptDecryptTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java index 51c117a..df824ca 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -327,4 +327,15 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .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()); + } } From 34a05e96a103f392fff06dc9d901ebe611d32db9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:45:14 +0100 Subject: [PATCH 109/238] Move signature verification operations to sopv interface subset --- .../main/kotlin/sop/external/ExternalSOPV.kt | 53 ++++++++++ .../sop/external/operation/VersionExternal.kt | 4 + .../main/kotlin/sop/cli/picocli/SopVCLI.kt | 98 +++++++++++++++++++ .../sop/cli/picocli/commands/VersionCmd.kt | 6 ++ .../src/main/resources/msg_sop.properties | 2 + .../src/main/resources/msg_sop_de.properties | 2 + sop-java/src/main/kotlin/sop/SOP.kt | 26 +---- sop-java/src/main/kotlin/sop/SOPV.kt | 34 +++++++ .../src/main/kotlin/sop/operation/Version.kt | 10 ++ .../sop/testsuite/operation/VersionTest.java | 14 +++ 10 files changed, 224 insertions(+), 25 deletions(-) create mode 100644 external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt create mode 100644 sop-java/src/main/kotlin/sop/SOPV.kt diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt new file mode 100644 index 0000000..f22f947 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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() } + } + } +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt index 7e13fc1..728f3b6 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/VersionExternal.kt @@ -68,6 +68,10 @@ class VersionExternal(binary: String, environment: Properties) : Version { return null } + override fun getSopVVersion(): String { + return executeForLines(commandList.plus("--sopv")) + } + override fun getSopSpecVersion(): String { return executeForLines(commandList.plus("--sop-spec")) } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt new file mode 100644 index 0000000..9a8b4b4 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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 Picocli Readme + */ + @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 = + mutableListOf() // ignore any other parameters and options in the first parsing phase + } +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt index 75197fe..8b1936a 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/VersionCmd.kt @@ -22,6 +22,7 @@ class VersionCmd : AbstractSopCmd() { @Option(names = ["--extended"]) var extended: Boolean = false @Option(names = ["--backend"]) var backend: Boolean = false @Option(names = ["--sop-spec"]) var sopSpec: Boolean = false + @Option(names = ["--sopv"]) var sopv: Boolean = false } override fun run() { @@ -47,5 +48,10 @@ class VersionCmd : AbstractSopCmd() { println(version.getSopSpecVersion()) return } + + if (exclusive!!.sopv) { + println(version.getSopVVersion()) + return + } } } diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 52c5368..7979eb3 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Locale for description texts # Generic diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 5900f39..40a316d 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -2,7 +2,9 @@ # # SPDX-License-Identifier: Apache-2.0 sop.name=sop +sopv.name=sopv usage.header=Stateless OpenPGP Protocol +sopv.usage.header=Stateless OpenPGP Protocol - Signature Verification Interface Subset locale=Gebietsschema für Beschreibungstexte # Generic diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index e01763a..7fdd414 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -9,16 +9,13 @@ 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 @@ -26,10 +23,7 @@ import sop.operation.Version * intended for reuse. If you for example need to generate multiple keys, make a dedicated call to * [generateKey] once per key generation. */ -interface SOP { - - /** Get information about the implementations name and version. */ - fun version(): Version +interface SOP : SOPV { /** Generate a secret key. */ fun generateKey(): GenerateKey @@ -53,24 +47,6 @@ interface SOP { */ 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. */ fun inlineDetach(): InlineDetach diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt new file mode 100644 index 0000000..d331559 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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 +} diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 9b3bd8a..5f26491 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -4,6 +4,9 @@ package sop.operation +import kotlin.jvm.Throws +import sop.exception.SOPGPException + interface Version { /** @@ -97,4 +100,11 @@ interface Version { * @return remarks or null */ 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 } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java index 0b19d20..f836935 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java @@ -10,6 +10,7 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.opentest4j.TestAbortedException; import sop.SOP; +import sop.exception.SOPGPException; import java.util.stream.Stream; @@ -72,4 +73,17 @@ public class VersionTest extends AbstractSOPTest { int sopRevision = sop.version().getSopSpecRevisionNumber(); 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."); + } + } } From ae2389cabf9dbab2972aa1aa40cc42042ee5cf92 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 17 Mar 2024 15:45:29 +0100 Subject: [PATCH 110/238] Bump version to 10.0.0 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 434fdc5..80c8092 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '8.0.2' + shortVersion = '10.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From 1df57475493a18df3303f0162ec5c98259d53387 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:37:46 +0200 Subject: [PATCH 111/238] EncryptExternal: Fix parameter passing for --sign-with option --- .../src/main/kotlin/sop/external/operation/EncryptExternal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt index 6f1cc6c..12d9cff 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -36,7 +36,7 @@ class EncryptExternal( override fun mode(mode: EncryptAs): Encrypt = apply { commandList.add("--as=$mode") } override fun signWith(key: InputStream): Encrypt = apply { - commandList.add("--sign-with@ENV:SIGN_WITH_$argCounter") + commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter") envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") argCounter += 1 } From 4388f00dc08e936a402d79db52e3bdad47f0c71b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:00:30 +0200 Subject: [PATCH 112/238] Fix NPE in DecryptExternal when reading lines --- .../main/kotlin/sop/external/operation/DecryptExternal.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt index b68d3a6..e0a900d 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -108,8 +108,8 @@ class DecryptExternal( finish(process) val sessionKeyOutIn = FileInputStream(sessionKeyOut) - var line = readString(sessionKeyOutIn) - val sessionKey = SessionKey.fromString(line.trim { it <= ' ' }) + var line: String? = readString(sessionKeyOutIn) + val sessionKey = line?.let { l -> SessionKey.fromString(l.trim { it <= ' ' }) } sessionKeyOutIn.close() sessionKeyOut.delete() @@ -118,7 +118,7 @@ class DecryptExternal( val verifyOutIn = FileInputStream(verifyOut) val reader = BufferedReader(InputStreamReader(verifyOutIn)) while (reader.readLine().also { line = it } != null) { - verifications.add(Verification.fromString(line.trim())) + line?.let { verifications.add(Verification.fromString(it.trim())) } } reader.close() } From 65945e0094fa6a4a1e22cb0d6c71f0e745193c1e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 12:41:40 +0200 Subject: [PATCH 113/238] Fix external-sop decrypt --verifications-out --- .../src/main/kotlin/sop/external/operation/DecryptExternal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt index e0a900d..1e6d6a2 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/DecryptExternal.kt @@ -78,7 +78,7 @@ class DecryptExternal( val verifyOut = File(tempDir, "verifications-out") verifyOut.delete() if (requireVerification) { - commandList.add("--verify-out=${verifyOut.absolutePath}") + commandList.add("--verifications-out=${verifyOut.absolutePath}") } try { From 2d4bc24c64b1a7e68a2e6596e5e07a63b940ccd5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:29:40 +0200 Subject: [PATCH 114/238] Abort tests on UnsupportedOption --- .../testsuite/operation/AbstractSOPTest.java | 5 +++++ .../sop/testsuite/AbortOnUnsupportedOption.kt | 12 +++++++++++ .../AbortOnUnsupportedOptionExtension.kt | 21 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt create mode 100644 sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java index 8595898..6c163f7 100644 --- a/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java @@ -5,8 +5,11 @@ package sop.testsuite.operation; import org.junit.jupiter.api.Named; +import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; import sop.SOP; +import sop.testsuite.AbortOnUnsupportedOption; +import sop.testsuite.AbortOnUnsupportedOptionExtension; import sop.testsuite.SOPInstanceFactory; import java.lang.reflect.InvocationTargetException; @@ -15,6 +18,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; +@ExtendWith(AbortOnUnsupportedOptionExtension.class) +@AbortOnUnsupportedOption public abstract class AbstractSOPTest { private static final List backends = new ArrayList<>(); diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt new file mode 100644 index 0000000..cf99671 --- /dev/null +++ b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt @@ -0,0 +1,12 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite + +import java.lang.annotation.Inherited + +@Target(AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@Inherited +annotation class AbortOnUnsupportedOption diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt new file mode 100644 index 0000000..809c78f --- /dev/null +++ b/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite + +import org.junit.jupiter.api.Assumptions +import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler +import sop.exception.SOPGPException + +class AbortOnUnsupportedOptionExtension : TestExecutionExceptionHandler { + override fun handleTestExecutionException(context: ExtensionContext, throwable: Throwable) { + val testClass = context.requiredTestClass + val annotation = testClass.getAnnotation(AbortOnUnsupportedOption::class.java) + if (annotation != null && SOPGPException.UnsupportedOption::class.isInstance(throwable)) { + Assumptions.assumeTrue(false, "Test aborted due to: " + throwable.message) + } + throw throwable + } +} From d25a424adc1328c9e10ffd72ac55c8f95188fdf5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:16:11 +0200 Subject: [PATCH 115/238] Update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f188b0b..2c266f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ SPDX-License-Identifier: Apache-2.0 # 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 - `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. From a5232703959a9c6c1c1059d7052940044a518dad Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:43:12 +0100 Subject: [PATCH 116/238] Update spec revision and badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa7e5a5..0efd41f 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0 # SOP for Java [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) -[![Spec Revision: 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) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java) From 8d7e89098fe462d2a8b5246f45ac5d05d2a21c70 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:40:25 +0200 Subject: [PATCH 117/238] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c266f1..2cb5b9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ SPDX-License-Identifier: Apache-2.0 - Remove `label()` option from `Armor` operation - Fix exit code for 'Missing required option/parameter' error - Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option +- Fix `EncryptExternal` use of `--sign-with` parameter +- Fix `NullPointerException` in `DecryptExternal` when reading lines +- Fix `DecryptExternal` use of `verifications-out` +- Test suite: Ignore tests if `UnsupportedOption` is thrown ## 10.0.0 - Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). From f7530e3263b69c73ca1c45b4e2ca3a3d182127b9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:43:45 +0200 Subject: [PATCH 118/238] Bump logback to 1.4.14 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 80c8092..7572c34 100644 --- a/version.gradle +++ b/version.gradle @@ -12,7 +12,7 @@ allprojects { jsrVersion = '3.0.2' junitVersion = '5.8.2' junitSysExitVersion = '1.1.2' - logbackVersion = '1.2.11' + logbackVersion = '1.4.14' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From 261ac212b8f91ef58557f8e09278edd2dfc6ac81 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:44:17 +0200 Subject: [PATCH 119/238] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cb5b9d..37c38fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ SPDX-License-Identifier: Apache-2.0 - Fix `NullPointerException` in `DecryptExternal` when reading lines - Fix `DecryptExternal` use of `verifications-out` - Test suite: Ignore tests if `UnsupportedOption` is thrown +- Bump `logback-core` to `1.4.14` ## 10.0.0 - Update implementation to [SOP Specification revision 10](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-10.html). From 354ef8841aafe4aa0a66c76e70d5db5208223e7d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 13:46:48 +0200 Subject: [PATCH 120/238] SOP-Java 10.0.1 --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 7572c34..fbac859 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.0' - isSnapshot = true + shortVersion = '10.0.1' + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 7014dbcfb7bec164cf8e216eaeca09ca6ca56bf5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 14:36:41 +0200 Subject: [PATCH 121/238] SOP-Java 10.0.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index fbac859..a018d80 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.1' - isSnapshot = false + shortVersion = '10.0.2' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 63d80452247a4f596eb49904ee57028092a7cdd7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 14:41:34 +0200 Subject: [PATCH 122/238] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37c38fc..ff1f4cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.0.1-SNAPSHOT +## 10.0.1 - Remove `label()` option from `Armor` operation - Fix exit code for 'Missing required option/parameter' error - Fix `revoke-key`: Allow for multiple invocations of `--with-key-password` option From a90f9be0e44144e5545d0422455ef4cd85373f68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 15:50:31 +0200 Subject: [PATCH 123/238] Downgrade logback-core to 1.2.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index a018d80..65a8a27 100644 --- a/version.gradle +++ b/version.gradle @@ -12,7 +12,7 @@ allprojects { jsrVersion = '3.0.2' junitVersion = '5.8.2' junitSysExitVersion = '1.1.2' - logbackVersion = '1.4.14' + logbackVersion = '1.2.13' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From a09f10fe8547d906030bc11aab0f1fd0d48fed2b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 15:50:40 +0200 Subject: [PATCH 124/238] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1f4cc..493a3b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.0.2-SNAPSHOT +- Downgrade `logback-core` to `1.2.13` + ## 10.0.1 - Remove `label()` option from `Armor` operation - Fix exit code for 'Missing required option/parameter' error From 1958614fac828a52519f34a78429ac6e92c3d935 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:20:33 +0200 Subject: [PATCH 125/238] SOP-Java 10.0.2 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 493a3b9..6c0c0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.0.2-SNAPSHOT +## 10.0.2 - Downgrade `logback-core` to `1.2.13` ## 10.0.1 diff --git a/version.gradle b/version.gradle index 65a8a27..256871d 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.0.2' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From b3f446fe8d49aad790b7f0ed264d3491ff0a0d22 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:22:06 +0200 Subject: [PATCH 126/238] SOP-Java 10.0.3-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 256871d..db61bcc 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.2' - isSnapshot = false + shortVersion = '10.0.3' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 42a16a4f6d46199db279f6daf9a9b70859841b7b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 13:15:06 +0100 Subject: [PATCH 127/238] Fix password parameter passing in change-key-password --- .../sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt index 0c2eb4a..be37309 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ChangeKeyPasswordCmd.kt @@ -33,9 +33,15 @@ class ChangeKeyPasswordCmd : AbstractSopCmd() { changeKeyPassword.noArmor() } - oldKeyPasswords.forEach { changeKeyPassword.oldKeyPassphrase(it) } + oldKeyPasswords.forEach { + val password = stringFromInputStream(getInput(it)) + changeKeyPassword.oldKeyPassphrase(password) + } - newKeyPassword?.let { changeKeyPassword.newKeyPassphrase(it) } + newKeyPassword?.let { + val password = stringFromInputStream(getInput(it)) + changeKeyPassword.newKeyPassphrase(password) + } try { changeKeyPassword.keys(System.`in`).writeTo(System.out) From 375dd6578903eb23cac395fe86b861a86afbce60 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:13:12 +0200 Subject: [PATCH 128/238] revoke-key command: Allow for multiple '--with-key-password' options --- .../main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt index 0b93ac5..b9b1015 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/RevokeKeyCmd.kt @@ -19,8 +19,8 @@ class RevokeKeyCmd : AbstractSopCmd() { @Option(names = ["--no-armor"], negatable = true) var armor = true - @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") - var withKeyPassword: String? = null + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD", arity = "0..*") + var withKeyPassword: List = listOf() override fun run() { val revokeKey = throwIfUnsupportedSubcommand(SopCLI.getSop().revokeKey(), "revoke-key") @@ -29,9 +29,9 @@ class RevokeKeyCmd : AbstractSopCmd() { revokeKey.noArmor() } - withKeyPassword?.let { + for (passwordIn in withKeyPassword) { try { - val password = stringFromInputStream(getInput(it)) + val password = stringFromInputStream(getInput(passwordIn)) revokeKey.withKeyPassword(password) } catch (e: SOPGPException.UnsupportedOption) { val errorMsg = From f35fd6c1ae39cd9cf2b79bd0d5a541c191203e11 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 13:53:57 +0100 Subject: [PATCH 129/238] Update changelog --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c0c0c1..13a87c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.0.3-SNAPSHOT +- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) +- Backport: `revoke-key`: Allow for multiple password options + ## 10.0.2 - Downgrade `logback-core` to `1.2.13` @@ -25,6 +29,10 @@ SPDX-License-Identifier: Apache-2.0 - Introduce `sopv` interface subset with revision `1.0` - Add `sop version --sopv` +## 8.0.2 +- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) +- Backport: `revoke-key`: Allow for multiple password options + ## 8.0.1 - `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. @@ -43,6 +51,13 @@ SPDX-License-Identifier: Apache-2.0 - Change `EncryptAs` values into lowercase - Change `SignAs` values into lowercase +## 7.0.2 +- CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) +- Backport: revoke-key command: Allow for multiple '--with-key-password' options + +## 7.0.1 +- `decrypt`: Do not throw `NoSignature` exception (exit code 3) if `--verify-with` is provided, but `VERIFICATIONS` is empty. + ## 7.0.0 - Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html). - Add support for new `revoke-key` subcommand From c136d40fa7de9fa7e1979b1ffdd36f1fe44c9474 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 13:54:31 +0100 Subject: [PATCH 130/238] SOP-Java 10.0.3 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index db61bcc..396af1a 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.0.3' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From e6c9d6f43d3a37f0e9b89462d5f13a004b2c85f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 31 Oct 2024 14:06:37 +0100 Subject: [PATCH 131/238] SOP-Java 10.0.4-SNAPSHOT --- CHANGELOG.md | 2 +- version.gradle | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 13a87c3..feee975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.0.3-SNAPSHOT +## 10.0.3 - CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) - Backport: `revoke-key`: Allow for multiple password options diff --git a/version.gradle b/version.gradle index 396af1a..e8bcbef 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.0.3' - isSnapshot = false + shortVersion = '10.0.4' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From ac00b68694a62fa50c67717910dc59fb0d7e4e0a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 21 Mar 2024 13:43:25 +0100 Subject: [PATCH 132/238] Add description of external-sop module --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 0efd41f..baeb874 100644 --- a/README.md +++ b/README.md @@ -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-picocli](/sop-java-picocli) contains a wrapper application that transforms the `sop-java` API into a command line application compatible with the SOP-CLI specification. +* [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable, +allowing to delegate the implementation logic to an arbitrary SOP CLI implementation. ## Known Implementations (Please expand!) From e7778cb0d299ae5c479b538d29f8e639c71be2e7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 27 Mar 2024 21:50:01 +0100 Subject: [PATCH 133/238] Remove deprecated junit5-system-exit Replaced with custom test DSL that avoids System.exit --- sop-java-picocli/build.gradle | 5 +- .../test/java/sop/cli/picocli/SOPTest.java | 11 +- .../cli/picocli/commands/ArmorCmdTest.java | 12 +- .../cli/picocli/commands/DearmorCmdTest.java | 8 +- .../cli/picocli/commands/DecryptCmdTest.java | 137 +++++----- .../cli/picocli/commands/EncryptCmdTest.java | 121 +++++---- .../picocli/commands/ExtractCertCmdTest.java | 22 +- .../picocli/commands/GenerateKeyCmdTest.java | 30 ++- .../picocli/commands/InlineDetachCmdTest.java | 12 +- .../sop/cli/picocli/commands/SignCmdTest.java | 61 +++-- .../cli/picocli/commands/VerifyCmdTest.java | 70 ++++-- .../cli/picocli/commands/VersionCmdTest.java | 41 ++- .../assertions/SopExecutionAssertions.java | 235 ++++++++++++++++++ 13 files changed, 550 insertions(+), 215 deletions(-) create mode 100644 sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 438ef50..0596ad3 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -12,15 +12,12 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - // Testing Exit Codes in JUnit - // https://todd.ginsberg.com/post/testing-system-exit/ - testImplementation "com.ginsberg:junit5-system-exit:$junitSysExitVersion" - // Mocking Components testImplementation "org.mockito:mockito-core:$mockitoVersion" // SOP implementation(project(":sop-java")) + testImplementation(testFixtures(project(":sop-java"))) // CLI implementation "info.picocli:picocli:$picocliVersion" diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index 68b32be..fe49472 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -6,12 +6,13 @@ package sop.cli.picocli; import static org.junit.jupiter.api.Assertions.assertEquals; 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.Arrays; import java.util.List; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; @@ -34,20 +35,18 @@ import sop.operation.Version; public class SOPTest { @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedSubcommand.EXIT_CODE) public void assertExitOnInvalidSubcommand() { SOP sop = mock(SOP.class); SopCLI.setSopInstance(sop); - SopCLI.main(new String[] {"invalid"}); + assertUnsupportedSubcommand(() -> SopCLI.execute("invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertThrowsIfNoSOPBackendSet() { SopCLI.setSopInstance(null); - // At this point, no SOP backend is set, so an InvalidStateException triggers exit(1) - SopCLI.main(new String[] {"armor"}); + // At this point, no SOP backend is set, so an InvalidStateException triggers error code 1 + assertGenericError(() -> SopCLI.execute("armor")); } @Test diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java index da211e0..3dd4c7c 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java @@ -4,8 +4,6 @@ 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.Test; import sop.Ready; @@ -24,6 +22,8 @@ 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.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; public class ArmorCmdTest { @@ -42,24 +42,22 @@ public class ArmorCmdTest { @Test public void assertDataIsAlwaysCalled() throws SOPGPException.BadData, IOException { - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); verify(armor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void ifBadDataExit41() throws SOPGPException.BadData, IOException { when(armor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"armor"}); + assertBadData(() -> SopCLI.execute("armor")); } @Test - @FailOnSystemExit public void ifNoErrorsNoExit() { when(sop.armor()).thenReturn(armor); - SopCLI.main(new String[] {"armor"}); + assertSuccess(() -> SopCLI.execute("armor")); } private static Ready nopReady() { diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java index 875eaed..b0a9fd8 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DearmorCmdTest.java @@ -9,12 +9,13 @@ 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.assertBadData; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -48,14 +49,13 @@ public class DearmorCmdTest { @Test public void assertDataIsCalled() throws IOException, SOPGPException.BadData { - SopCLI.main(new String[] {"dearmor"}); + assertSuccess(() -> SopCLI.execute("dearmor")); verify(dearmor, times(1)).data((InputStream) any()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataCausesExit41() throws IOException, SOPGPException.BadData { when(dearmor.data((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException("invalid armor"))); - SopCLI.main(new String[] {"dearmor"}); + assertBadData(() -> SopCLI.execute("dearmor")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index 62070c2..b7cb8bc 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; 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.verify; 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 { @@ -74,47 +85,47 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void missingArgumentsExceptionCausesExit19() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.MissingArg("Missing arguments.")); - SopCLI.main(new String[] {"decrypt"}); + assertMissingArg(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataExceptionCausesExit41() throws SOPGPException.MissingArg, SOPGPException.BadData, SOPGPException.CannotDecrypt, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"decrypt"}); + assertBadData(() -> SopCLI.execute("decrypt")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); 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 public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { 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"); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("swordfish"); 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 public void assertDefaultTimeRangesAreUsedIfNotOverwritten() throws SOPGPException.UnsupportedOption { 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)).verifyNotAfter( ArgumentMatchers.argThat(argument -> { @@ -125,7 +136,8 @@ public class DecryptCmdTest { @Test 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)).verifyNotAfter(AbstractSopCmd.END_OF_TIME); } @@ -138,54 +150,57 @@ public class DecryptCmdTest { 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)).verifyNotBefore(ArgumentMatchers.argThat(isMaxOneSecOff)); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotBeforeCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-before", "invalid")); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedDateInNotAfterCausesExit1() { // ParserException causes exit(1) - SopCLI.main(new String[] {"decrypt", "--verify-not-after", "invalid"}); + assertGenericError(() -> + SopCLI.execute("decrypt", "--verify-not-after", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotAfterCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotAfter(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--verify-not-after", "now"}); + when(decrypt.verifyNotAfter(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting upper signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-after", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertUnsupportedNotBeforeCausesExit37() throws SOPGPException.UnsupportedOption { - when(decrypt.verifyNotBefore(any())).thenThrow(new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); - SopCLI.main(new String[] {"decrypt", "--verify-not-before", "now"}); + when(decrypt.verifyNotBefore(any())).thenThrow( + new SOPGPException.UnsupportedOption("Setting lower signature date boundary not supported.")); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--verify-not-before", "now")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void assertExistingSessionKeyOutFileCausesExit59() throws IOException { File tempFile = File.createTempFile("existing-session-key-", ".tmp"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertOutputExists(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void assertWhenSessionKeyCannotBeExtractedExit37() throws IOException { Path tempDir = Files.createTempDirectory("session-key-out-dir"); File tempFile = new File(tempDir.toFile(), "session-key"); tempFile.deleteOnExit(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", tempFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("decrypt", "--session-key-out", tempFile.getAbsolutePath())); } @Test @@ -210,8 +225,10 @@ public class DecryptCmdTest { File verificationsFile = new File(tempDir.toFile(), "verifications"); File keyFile = new File(tempDir.toFile(), "key.asc"); keyFile.createNewFile(); - SopCLI.main(new String[] {"decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), - "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", "--session-key-out", sessionKeyFile.getAbsolutePath(), + "--verifications-out", verificationsFile.getAbsolutePath(), "--verify-with", + keyFile.getAbsolutePath())); ByteArrayOutputStream bytesInFile = new ByteArrayOutputStream(); try (FileInputStream fileIn = new FileInputStream(sessionKeyFile)) { @@ -241,10 +258,10 @@ public class DecryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.CannotDecrypt.EXIT_CODE) public void assertUnableToDecryptExceptionResultsInExit29() throws SOPGPException.CannotDecrypt, SOPGPException.MissingArg, SOPGPException.BadData, IOException { when(decrypt.ciphertext((InputStream) any())).thenThrow(new SOPGPException.CannotDecrypt()); - SopCLI.main(new String[] {"decrypt"}); + assertCannotDecrypt(() -> + SopCLI.execute("decrypt")); } @Test @@ -258,30 +275,32 @@ public class DecryptCmdTest { 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 - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void badDataInVerifyWithCausesExit41() throws IOException, SOPGPException.BadData { when(decrypt.verifyWithCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); 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 - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void unexistentCertFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--verify-with", "invalid"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--verify-with", "invalid")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.OutputExists.EXIT_CODE) public void existingVerifyOutCausesExit59() throws IOException { File certFile = File.createTempFile("existing-verify-out-cert", ".asc"); 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 @@ -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))) { String line = reader.readLine(); assertEquals("2021-07-11T20:58:23Z 1B66A707819A920925BC6777C3E0AFC0B2DFF862 C8CD564EBF8D7BBA90611D8D071773658BF6BF86", line); @@ -320,66 +341,64 @@ public class DecryptCmdTest { File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile1.getAbsolutePath(), - "--with-session-key", sessionKeyFile2.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile1.getAbsolutePath(), + "--with-session-key", sessionKeyFile2.getAbsolutePath())); verify(decrypt).withSessionKey(key1); verify(decrypt).withSessionKey(key2); } @Test - @ExpectSystemExitWithStatus(1) public void assertMalformedSessionKeysResultInExit1() throws IOException { File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); - SopCLI.main(new String[] {"decrypt", - "--with-session-key", sessionKeyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("decrypt", + "--with-session-key", sessionKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void assertBadDataInKeysResultsInExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData, IOException { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertBadData(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertKeyFileNotFoundCausesExit61() { - SopCLI.main(new String[] {"decrypt", "nonexistent-key"}); + assertMissingInput(() -> SopCLI.execute("decrypt", "nonexistent-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void assertProtectedKeyCausesExit67() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.BadData { when(decrypt.withKey((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) 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())); File tempKeyFile = File.createTempFile("key-", ".tmp"); - SopCLI.main(new String[] {"decrypt", tempKeyFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("decrypt", tempKeyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingPassphraseFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-password", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-password", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void assertMissingSessionKeyFileCausesExit61() { - SopCLI.main(new String[] {"decrypt", "--with-session-key", "missing"}); + assertMissingInput(() -> + SopCLI.execute("decrypt", "--with-session-key", "missing")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.IncompleteVerification.EXIT_CODE) public void verifyOutWithoutVerifyWithCausesExit23() { - SopCLI.main(new String[] {"decrypt", "--verifications-out", "out.file"}); + assertIncompleteVerification(() -> + SopCLI.execute("decrypt", "--verifications-out", "out.file")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java index 09346af..85ae052 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/EncryptCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; 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.verify; 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 { @@ -50,48 +60,50 @@ public class EncryptCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) - public void missingBothPasswordAndCertFileCauseExit19() { - SopCLI.main(new String[] {"encrypt", "--no-armor"}); + public void missingBothPasswordAndCertFileCausesMissingArg() { + assertMissingArg(() -> + SopCLI.execute("encrypt", "--no-armor")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_unsupportedEncryptAsCausesExit37() throws SOPGPException.UnsupportedOption { + public void as_unsupportedEncryptAsCausesUnsupportedOption() throws SOPGPException.UnsupportedOption { 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 - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void as_invalidModeOptionCausesExit37() { - SopCLI.main(new String[] {"encrypt", "--as", "invalid"}); + public void as_invalidModeOptionCausesUnsupportedOption() { + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--as", "invalid")); } @Test public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); 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); } } @Test - @ExpectSystemExitWithStatus(SOPGPException.PasswordNotHumanReadable.EXIT_CODE) - public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_notHumanReadablePasswordCausesPWNotHumanReadable() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertPasswordNotHumanReadable(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) - public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + public void withPassword_unsupportedWithPasswordCausesUnsupportedOption() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); File passwordFile = TestFileUtil.writeTempStringFile("orange"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } @Test @@ -99,99 +111,107 @@ public class EncryptCmdTest { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); 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()); } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void signWith_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc"}); + public void signWith_nonExistentKeyFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "--with-password", "admin", "--sign-with", "nonExistent.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) - public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_keyIsProtectedCausesKeyIsProtected() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); 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 - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_unsupportedAsymmetricAlgoCausesUnsupportedAsymAlgo() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File keyFile = File.createTempFile("sign-with", ".asc"); 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 - @ExpectSystemExitWithStatus(SOPGPException.KeyCannotSign.EXIT_CODE) - public void signWith_certCannotSignCausesExit79() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { + public void signWith_certCannotSignCausesKeyCannotSign() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); File keyFile = File.createTempFile("sign-with", ".asc"); 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 - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { + public void signWith_badDataCausesBadData() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File keyFile = File.createTempFile("sign-with", ".asc"); 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 - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) - public void cert_nonExistentCertFileCausesExit61() { - SopCLI.main(new String[] {"encrypt", "invalid.asc"}); + public void cert_nonExistentCertFileCausesMissingInput() { + assertMissingInput(() -> + SopCLI.execute("encrypt", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) - public void cert_unsupportedAsymmetricAlgorithmCausesExit13() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_unsupportedAsymmetricAlgorithmCausesUnsupportedAsymAlg() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertUnsupportedAsymmetricAlgo(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.CertCannotEncrypt.EXIT_CODE) - public void cert_certCannotEncryptCausesExit17() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_certCannotEncryptCausesCertCannotEncrypt() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.CertCannotEncrypt("Certificate cannot encrypt.", new Exception())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertCertCannotEncrypt(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void cert_badDataCausesExit41() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { + public void cert_badDataCausesBadData() throws IOException, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.CertCannotEncrypt, SOPGPException.BadData { when(encrypt.withCert((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File certFile = File.createTempFile("cert", ".asc"); - SopCLI.main(new String[] {"encrypt", certFile.getAbsolutePath()}); + assertBadData(() -> + SopCLI.execute("encrypt", certFile.getAbsolutePath())); } @Test public void noArmor_notCalledByDefault() throws IOException { 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(); } @Test public void noArmor_callGetsPassedDown() throws IOException { 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(); } @Test - @ExpectSystemExitWithStatus(1) - public void writeTo_ioExceptionCausesExit1() throws IOException { + public void writeTo_ioExceptionCausesGenericError() throws IOException { when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult() { @Override public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException { @@ -199,6 +219,7 @@ public class EncryptCmdTest { } }); File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); - SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("encrypt", "--with-password", passwordFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java index 12f837d..3b046a0 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ExtractCertCmdTest.java @@ -10,12 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; 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.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.Ready; @@ -45,32 +47,34 @@ public class ExtractCertCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"extract-cert"}); + assertSuccess(() -> + SopCLI.execute("extract-cert")); verify(extractCert, never()).noArmor(); } @Test public void noArmor_passedDown() { - SopCLI.main(new String[] {"extract-cert", "--no-armor"}); + assertSuccess(() -> + SopCLI.execute("extract-cert", "--no-armor")); verify(extractCert, times(1)).noArmor(); } @Test - @ExpectSystemExitWithStatus(1) - public void key_ioExceptionCausesExit1() throws IOException, SOPGPException.BadData { + public void key_ioExceptionCausesGenericError() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"extract-cert"}); + assertGenericError(() -> + SopCLI.execute("extract-cert")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) - public void key_badDataCausesExit41() throws IOException, SOPGPException.BadData { + public void key_badDataCausesBadData() throws IOException, SOPGPException.BadData { when(extractCert.key((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); - SopCLI.main(new String[] {"extract-cert"}); + assertBadData(() -> + SopCLI.execute("extract-cert")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java index e7ebf1a..126c851 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/GenerateKeyCmdTest.java @@ -10,11 +10,14 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; 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.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -47,19 +50,22 @@ public class GenerateKeyCmdTest { @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"generate-key", "Alice"}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice")); verify(generateKey, never()).noArmor(); } @Test 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(); } @Test public void userId_multipleUserIdsPassedDownInProperOrder() { - SopCLI.main(new String[] {"generate-key", "Alice ", "Bob "}); + assertSuccess(() -> + SopCLI.execute("generate-key", "Alice ", "Bob ")); InOrder inOrder = Mockito.inOrder(generateKey); inOrder.verify(generateKey).userId("Alice "); @@ -69,30 +75,32 @@ public class GenerateKeyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) 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, // so we might want to change this test in the future. when(generateKey.generate()).thenThrow(new SOPGPException.MissingArg("Missing user-id.")); - SopCLI.main(new String[] {"generate-key"}); + assertMissingArg(() -> + SopCLI.execute("generate-key")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedAsymmetricAlgo.EXIT_CODE) public void unsupportedAsymmetricAlgorithmCausesExit13() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { 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 - @ExpectSystemExitWithStatus(1) - public void ioExceptionCausesExit1() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { + public void ioExceptionCausesGenericError() throws SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.MissingArg, IOException { when(generateKey.generate()).thenReturn(new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { throw new IOException(); } }); - SopCLI.main(new String[] {"generate-key", "Alice"}); + + assertGenericError(() -> + SopCLI.execute("generate-key", "Alice")); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java index 3a16c61..a230aaa 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/InlineDetachCmdTest.java @@ -4,7 +4,6 @@ package sop.cli.picocli.commands; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.ReadyWithResult; @@ -26,6 +25,8 @@ 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.assertMissingArg; +import static sop.testsuite.assertions.SopExecutionAssertions.assertSuccess; public class InlineDetachCmdTest { @@ -41,9 +42,9 @@ public class InlineDetachCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) - public void testMissingSignaturesOutResultsInExit19() { - SopCLI.main(new String[] {"inline-detach"}); + public void testMissingSignaturesOutResultsInMissingArg() { + assertMissingArg(() -> + SopCLI.execute("inline-detach")); } @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)).message((InputStream) any()); } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java index c3d6b59..324d39a 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/SignCmdTest.java @@ -10,13 +10,20 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; 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.IOException; import java.io.InputStream; import java.io.OutputStream; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import sop.ReadyWithResult; @@ -54,70 +61,77 @@ public class SignCmdTest { @Test public void as_optionsAreCaseInsensitive() { - SopCLI.main(new String[] {"sign", "--as", "Binary", keyFile.getAbsolutePath()}); - SopCLI.main(new String[] {"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())); + assertSuccess(() -> + SopCLI.execute("sign", "--as", "BINARY", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_invalidOptionCausesExit37() { - SopCLI.main(new String[] {"sign", "--as", "Invalid", keyFile.getAbsolutePath()}); + assertUnsupportedOption(() -> + SopCLI.execute("sign", "--as", "Invalid", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void as_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { 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 - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void key_nonExistentKeyFileCausesExit61() { - SopCLI.main(new String[] {"sign", "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("sign", "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.KeyIsProtected.EXIT_CODE) public void key_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { when(detachedSign.key((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertKeyIsProtected(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void key_badDataCausesExit41() throws SOPGPException.KeyIsProtected, IOException, SOPGPException.BadData { 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 - @ExpectSystemExitWithStatus(SOPGPException.MissingArg.EXIT_CODE) public void key_missingKeyFileCausesExit19() { - SopCLI.main(new String[] {"sign"}); + assertMissingArg(() -> + SopCLI.execute("sign")); } @Test public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertSuccess(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); verify(detachedSign, never()).noArmor(); } @Test 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(); } @Test 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"); } @Test - @ExpectSystemExitWithStatus(1) public void data_ioExceptionCausesExit1() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenReturn(new ReadyWithResult() { @Override @@ -125,13 +139,14 @@ public class SignCmdTest { throw new IOException(); } }); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertGenericError(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.ExpectedText.EXIT_CODE) public void data_expectedTextExceptionCausesExit53() throws IOException, SOPGPException.ExpectedText { when(detachedSign.data((InputStream) any())).thenThrow(new SOPGPException.ExpectedText()); - SopCLI.main(new String[] {"sign", keyFile.getAbsolutePath()}); + assertExpectedText(() -> + SopCLI.execute("sign", keyFile.getAbsolutePath())); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java index 50a8043..3c9724f 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VerifyCmdTest.java @@ -10,6 +10,11 @@ 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.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.File; @@ -21,7 +26,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; -import com.ginsberg.junit.exit.ExpectSystemExitWithStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -76,60 +80,75 @@ public class VerifyCmdTest { @Test public void notAfter_passedDown() throws SOPGPException.UnsupportedOption, ParseException { 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); } @Test public void notAfter_now() throws SOPGPException.UnsupportedOption { 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)); } @Test 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); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notAfter_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { 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 public void notBefore_passedDown() throws SOPGPException.UnsupportedOption, ParseException { 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); } @Test public void notBefore_now() throws SOPGPException.UnsupportedOption { 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)); } @Test 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); } @Test - @ExpectSystemExitWithStatus(SOPGPException.UnsupportedOption.EXIT_CODE) public void notBefore_unsupportedOptionCausesExit37() throws SOPGPException.UnsupportedOption { 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 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)).notBefore(AbstractSopCmd.BEGINNING_OF_TIME); } @@ -139,43 +158,43 @@ public class VerifyCmdTest { } @Test - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void cert_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", signature.getAbsolutePath(), "invalid.asc"}); + assertMissingInput(() -> + SopCLI.execute("verify", signature.getAbsolutePath(), "invalid.asc")); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void cert_badDataCausesExit41() throws SOPGPException.BadData, 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 - @ExpectSystemExitWithStatus(SOPGPException.MissingInput.EXIT_CODE) public void signature_fileNotFoundCausesExit61() { - SopCLI.main(new String[] {"verify", "invalid.sig", cert.getAbsolutePath()}); + assertMissingInput(() -> + SopCLI.execute("verify", "invalid.sig", cert.getAbsolutePath())); } @Test - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void signature_badDataCausesExit41() throws SOPGPException.BadData, 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 - @ExpectSystemExitWithStatus(SOPGPException.NoSignature.EXIT_CODE) public void data_noSignaturesCausesExit3() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { 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 - @ExpectSystemExitWithStatus(SOPGPException.BadData.EXIT_CODE) public void data_badDataCausesExit41() throws SOPGPException.NoSignature, IOException, SOPGPException.BadData { 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 @@ -192,7 +211,8 @@ public class VerifyCmdTest { ByteArrayOutputStream out = new ByteArrayOutputStream(); 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); diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java index e284e35..92850bd 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/VersionCmdTest.java @@ -4,19 +4,19 @@ 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.Test; import sop.SOP; import sop.cli.picocli.SopCLI; -import sop.exception.SOPGPException; 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 { private Version version; @@ -29,6 +29,8 @@ public class VersionCmdTest { when(version.getVersion()).thenReturn("1.0"); when(version.getExtendedVersion()).thenReturn("MockSop Extended Version Information"); 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); SopCLI.setSopInstance(sop); @@ -36,26 +38,41 @@ public class VersionCmdTest { @Test public void assertVersionCommandWorks() { - SopCLI.main(new String[] {"version"}); + assertSuccess(() -> + SopCLI.execute("version")); verify(version, times(1)).getVersion(); verify(version, times(1)).getName(); } @Test public void assertExtendedVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--extended"}); + assertSuccess(() -> + SopCLI.execute("version", "--extended")); verify(version, times(1)).getExtendedVersion(); } @Test public void assertBackendVersionCommandWorks() { - SopCLI.main(new String[] {"version", "--backend"}); + assertSuccess(() -> + SopCLI.execute("version", "--backend")); verify(version, times(1)).getBackendVersion(); } @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() { - SopCLI.main(new String[] {"version", "--invalid"}); + assertUnsupportedOption(() -> + SopCLI.execute("version", "--invalid")); } } diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java b/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java new file mode 100644 index 0000000..bd07f0b --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java @@ -0,0 +1,235 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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()); + } +} From bb026bcbebcffeda0333333f3725f00e769e24f5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 27 Mar 2024 21:57:04 +0100 Subject: [PATCH 134/238] Mark ProxyOutputStream as deprecated --- sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index da6c4fa..a608c89 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -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 * target output stream is not yet known. */ +@Deprecated("Marked for removal.") class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null From 547acdb740e5dae2f24dca06795794ea9dc7c640 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 30 Mar 2024 19:00:09 +0100 Subject: [PATCH 135/238] Remove label() option from armor() operation --- .../kotlin/sop/external/operation/ArmorExternal.kt | 3 --- .../src/main/resources/msg_armor.properties | 1 - .../src/main/resources/msg_armor_de.properties | 1 - sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt | 14 -------------- sop-java/src/main/kotlin/sop/operation/Armor.kt | 12 ------------ 5 files changed, 31 deletions(-) delete mode 100644 sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt diff --git a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt index f80c57b..b202746 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/ArmorExternal.kt @@ -7,7 +7,6 @@ package sop.external.operation import java.io.InputStream import java.util.Properties import sop.Ready -import sop.enums.ArmorLabel import sop.exception.SOPGPException import sop.external.ExternalSOP import sop.operation.Armor @@ -18,8 +17,6 @@ class ArmorExternal(binary: String, environment: Properties) : Armor { private val commandList: MutableList = mutableListOf(binary, "armor") private val envList: List = ExternalSOP.propertiesToEnv(environment) - override fun label(label: ArmorLabel): Armor = apply { commandList.add("--label=$label") } - @Throws(SOPGPException.BadData::class) override fun data(data: InputStream): Ready = ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, data) diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index 2f4e217..b4dcb59 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Add ASCII Armor to standard input -label=Label to be used in the header and tail of the armoring stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 diff --git a/sop-java-picocli/src/main/resources/msg_armor_de.properties b/sop-java-picocli/src/main/resources/msg_armor_de.properties index a2303e9..4c365a8 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -2,7 +2,6 @@ # # SPDX-License-Identifier: Apache-2.0 usage.header=Schütze Standard-Eingabe mit ASCII Armor -label=Label für Kopf- und Fußzeile der ASCII Armor stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 diff --git a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt b/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt deleted file mode 100644 index 8b4e2cd..0000000 --- a/sop-java/src/main/kotlin/sop/enums/ArmorLabel.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.enums - -@Deprecated("Use of armor labels is deprecated.") -enum class ArmorLabel { - auto, - sig, - key, - cert, - message -} diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt index e89708b..be7f1a3 100644 --- a/sop-java/src/main/kotlin/sop/operation/Armor.kt +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -7,22 +7,10 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.enums.ArmorLabel import sop.exception.SOPGPException.BadData -import sop.exception.SOPGPException.UnsupportedOption 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. * From eadea08d3c6cdfb67b0d9699e00e1f41df7f5851 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Jul 2024 14:29:22 +0200 Subject: [PATCH 136/238] Add new SOPGPException types related to hardware modules --- .../kotlin/sop/exception/SOPGPException.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 2473258..bc9131f 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -305,4 +305,36 @@ abstract class SOPGPException : RuntimeException { 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 + } + } } From a8a753536a3e438b6fc8cd4a4fa2d99d646cdcb0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 9 Jul 2024 14:39:03 +0200 Subject: [PATCH 137/238] Add translations for new hardware exception error messages --- sop-java-picocli/src/main/resources/msg_sop.properties | 2 ++ sop-java-picocli/src/main/resources/msg_sop_de.properties | 2 ++ 2 files changed, 4 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 7979eb3..94e4dc0 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -36,6 +36,8 @@ usage.exitCodeList.17=73:Ambiguous input (a filename matching the designator alr usage.exitCodeList.18=79:Key is not signing capable 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.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 ## SHARED RESOURCES stacktrace=Print stacktrace diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 40a316d..786fa36 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -36,6 +36,8 @@ 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.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.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 ## SHARED RESOURCES stacktrace=Stacktrace ausgeben From 1fd316185184f3981f16741040d64a18ccafd6f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:20:33 +0200 Subject: [PATCH 138/238] Properly match MissingArg exception code --- .../main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt index 29aa77b..5778bb9 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SOPExceptionExitCodeMapper.kt @@ -21,6 +21,8 @@ class SOPExceptionExitCodeMapper : IExitCodeExceptionMapper { // Unmatched subcommand SOPGPException.UnsupportedSubcommand.EXIT_CODE } + } else if (exception is MissingParameterException) { + SOPGPException.MissingArg.EXIT_CODE } else if (exception is ParameterException) { // Invalid option (e.g. `--as invalid`) SOPGPException.UnsupportedOption.EXIT_CODE From 471947ef9ccbf1a7cce8056c0367adff7e1b2c19 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:38:59 +0200 Subject: [PATCH 139/238] Fix woodpecker warnings --- .woodpecker/build.yml | 2 ++ .woodpecker/reuse.yml | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index ff59c4e..fab075a 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -1,5 +1,7 @@ steps: run: + when: + event: push image: gradle:7.6-jdk11-jammy commands: # Install Sequoia-SOP diff --git a/.woodpecker/reuse.yml b/.woodpecker/reuse.yml index d78c61e..b278a39 100644 --- a/.woodpecker/reuse.yml +++ b/.woodpecker/reuse.yml @@ -2,6 +2,8 @@ # See https://reuse.software/ steps: reuse: + when: + event: push image: fsfe/reuse:latest commands: - - reuse lint \ No newline at end of file + - reuse lint From 594b9029b252af8f322852b4219534f7c042a056 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Oct 2024 16:33:31 +0200 Subject: [PATCH 140/238] Document logback spam --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index e8bcbef..37c5577 100644 --- a/version.gradle +++ b/version.gradle @@ -12,7 +12,7 @@ allprojects { jsrVersion = '3.0.2' junitVersion = '5.8.2' junitSysExitVersion = '1.1.2' - logbackVersion = '1.2.13' + logbackVersion = '1.2.13' // 1.4+ cause CLI spam mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From 4eb6d1fdcb418b2bb02e86f9818fb4faa623cc04 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 Dec 2024 20:39:54 +0100 Subject: [PATCH 141/238] Prevent unmatched parameters when setting locale --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 1d5d46b..4ec3d3f 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -74,7 +74,9 @@ class SopCLI { @JvmStatic fun execute(vararg args: String): Int { // Set locale - CommandLine(InitLocale()).parseArgs(*args) + CommandLine(InitLocale()) + .setUnmatchedArgumentsAllowed(true) + .parseArgs(*args) // Re-set bundle with updated locale cliMsg = ResourceBundle.getBundle("msg_sop") From ca65cbe668c79f066c9a19234fa734ad4e234ff6 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 12 Dec 2024 20:40:19 +0100 Subject: [PATCH 142/238] For now, do not re-set msg bundle (graal) --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 4ec3d3f..fcc7e74 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -83,8 +83,6 @@ class SopCLI { return CommandLine(SopCLI::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 From b3b8da4e358fa02b2009a073a2c4a00bbc3a48ab Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 13 Dec 2024 16:41:01 +0100 Subject: [PATCH 143/238] Move testfixtures to own artifact --- external-sop/build.gradle | 2 +- settings.gradle | 1 + sop-java-picocli/build.gradle | 2 +- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 4 +--- sop-java-testfixtures/build.gradle | 24 +++++++++++++++++++ .../src/main}/java/sop/testsuite/JUtils.java | 0 .../sop/testsuite/SOPInstanceFactory.java | 0 .../main}/java/sop/testsuite/TestData.java | 0 .../assertions/SopExecutionAssertions.java | 0 .../assertions/VerificationAssert.java | 0 .../assertions/VerificationListAssert.java | 0 .../testsuite/assertions/package-info.java | 0 .../testsuite/operation/AbstractSOPTest.java | 0 .../testsuite/operation/ArmorDearmorTest.java | 0 .../operation/ChangeKeyPasswordTest.java | 0 .../operation/DecryptWithSessionKeyTest.java | 0 .../DetachedSignDetachedVerifyTest.java | 0 .../operation/EncryptDecryptTest.java | 0 .../testsuite/operation/ExtractCertTest.java | 0 .../testsuite/operation/GenerateKeyTest.java | 0 ...ineSignInlineDetachDetachedVerifyTest.java | 0 .../operation/InlineSignInlineVerifyTest.java | 0 .../testsuite/operation/ListProfilesTest.java | 0 .../testsuite/operation/RevokeKeyTest.java | 0 .../sop/testsuite/operation/VersionTest.java | 0 .../sop/testsuite/operation/package-info.java | 0 .../java/sop/testsuite/package-info.java | 0 .../sop/testsuite/AbortOnUnsupportedOption.kt | 0 .../AbortOnUnsupportedOptionExtension.kt | 0 sop-java/build.gradle | 4 +--- 30 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 sop-java-testfixtures/build.gradle rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/JUtils.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/SOPInstanceFactory.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/TestData.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/SopExecutionAssertions.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/VerificationAssert.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/VerificationListAssert.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/assertions/package-info.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/AbstractSOPTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ArmorDearmorTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ChangeKeyPasswordTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/EncryptDecryptTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ExtractCertTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/GenerateKeyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/ListProfilesTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/RevokeKeyTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/VersionTest.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/operation/package-info.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/java/sop/testsuite/package-info.java (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt (100%) rename {sop-java/src/testFixtures => sop-java-testfixtures/src/main}/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt (100%) diff --git a/external-sop/build.gradle b/external-sop/build.gradle index 1bb86fc..d1a7ffb 100644 --- a/external-sop/build.gradle +++ b/external-sop/build.gradle @@ -27,7 +27,7 @@ dependencies { // The ExternalTestSubjectFactory reads json config file to find configured SOP binaries... testImplementation "com.google.code.gson:gson:$gsonVersion" // ...and extends TestSubjectFactory - testImplementation(testFixtures(project(":sop-java"))) + testImplementation(project(":sop-java-testfixtures")) } test { diff --git a/settings.gradle b/settings.gradle index 5dc6372..1cb66be 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,5 +6,6 @@ rootProject.name = 'SOP-Java' include 'sop-java', 'sop-java-picocli', + 'sop-java-testfixtures', 'external-sop' diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index 0596ad3..dbf0cc1 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -17,7 +17,7 @@ dependencies { // SOP implementation(project(":sop-java")) - testImplementation(testFixtures(project(":sop-java"))) + testImplementation(project(":sop-java-testfixtures")) // CLI implementation "info.picocli:picocli:$picocliVersion" diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index fcc7e74..b919370 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -74,9 +74,7 @@ class SopCLI { @JvmStatic fun execute(vararg args: String): Int { // Set locale - CommandLine(InitLocale()) - .setUnmatchedArgumentsAllowed(true) - .parseArgs(*args) + CommandLine(InitLocale()).setUnmatchedArgumentsAllowed(true).parseArgs(*args) // Re-set bundle with updated locale cliMsg = ResourceBundle.getBundle("msg_sop") diff --git a/sop-java-testfixtures/build.gradle b/sop-java-testfixtures/build.gradle new file mode 100644 index 0000000..d3d4a1e --- /dev/null +++ b/sop-java-testfixtures/build.gradle @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":sop-java")) + implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + // @Nullable, @Nonnull annotations + implementation "com.google.code.findbugs:jsr305:3.0.2" + +} diff --git a/sop-java/src/testFixtures/java/sop/testsuite/JUtils.java b/sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/JUtils.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/JUtils.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory.java b/sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/SOPInstanceFactory.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/SOPInstanceFactory.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/TestData.java b/sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/TestData.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/TestData.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/SopExecutionAssertions.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/SopExecutionAssertions.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationAssert.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/VerificationListAssert.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationListAssert.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/assertions/package-info.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/assertions/package-info.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/AbstractSOPTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ArmorDearmorTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ArmorDearmorTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ChangeKeyPasswordTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/EncryptDecryptTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ExtractCertTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ExtractCertTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/GenerateKeyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/ListProfilesTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/VersionTest.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/operation/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/operation/package-info.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/operation/package-info.java diff --git a/sop-java/src/testFixtures/java/sop/testsuite/package-info.java b/sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java similarity index 100% rename from sop-java/src/testFixtures/java/sop/testsuite/package-info.java rename to sop-java-testfixtures/src/main/java/sop/testsuite/package-info.java diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt similarity index 100% rename from sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt rename to sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt diff --git a/sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt similarity index 100% rename from sop-java/src/testFixtures/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt rename to sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt diff --git a/sop-java/build.gradle b/sop-java/build.gradle index ca546bf..a235a35 100644 --- a/sop-java/build.gradle +++ b/sop-java/build.gradle @@ -4,7 +4,6 @@ plugins { id 'java-library' - id 'java-test-fixtures' } group 'org.pgpainless' @@ -17,8 +16,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" - testFixturesImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" - testFixturesImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + testImplementation(project(":sop-java-testfixtures")) // @Nullable, @Nonnull annotations implementation "com.google.code.findbugs:jsr305:3.0.2" From b1e1a2283f82ab63c393b6c2f5efa6b5ffc0b676 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sun, 15 Dec 2024 18:19:30 +0100 Subject: [PATCH 144/238] Update changelog --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index feee975..367064c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.1.0-SNAPSHOT +- `sop-java`: + - Remove `label()` option from `armor()` subcommand + - Move test-fixtures artifact built with the `testFixtures` plugin into + its own module `sop-java-testfixtures`, which can be consumed by maven builds. +- `sop-java-picocli`: + - Properly map `MissingParameterException` to `MissingArg` exit code + - As a workaround for native builds using graalvm: + - Do not re-set message bundles dynamically (fails in native builds) + - Prevent an unmatched argument error + ## 10.0.3 - CLI `change-key-password`: Fix indirect parameter passing for new and old passwords (thanks to @dkg for the report) - Backport: `revoke-key`: Allow for multiple password options From 84e381fe8eac974547cdd4f39804d9a9a24b1b48 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Sat, 21 Dec 2024 14:49:45 +0100 Subject: [PATCH 145/238] Write sop-java version to sop-java-version.properties --- sop-java/build.gradle | 9 +++++++++ sop-java/src/main/resources/sop-java-version.properties | 1 + 2 files changed, 10 insertions(+) create mode 100644 sop-java/src/main/resources/sop-java-version.properties diff --git a/sop-java/build.gradle b/sop-java/build.gradle index a235a35..c6f4e4e 100644 --- a/sop-java/build.gradle +++ b/sop-java/build.gradle @@ -1,7 +1,10 @@ +import org.apache.tools.ant.filters.ReplaceTokens + // SPDX-FileCopyrightText: 2021 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 +import org.apache.tools.ant.filters.* plugins { id 'java-library' } @@ -23,6 +26,12 @@ dependencies { } +processResources { + filter ReplaceTokens, tokens: [ + "project.version": project.version.toString() + ] +} + test { useJUnitPlatform() } diff --git a/sop-java/src/main/resources/sop-java-version.properties b/sop-java/src/main/resources/sop-java-version.properties new file mode 100644 index 0000000..5f6f682 --- /dev/null +++ b/sop-java/src/main/resources/sop-java-version.properties @@ -0,0 +1 @@ +sop-java-version=@project.version@ \ No newline at end of file From 2b6015f59ad00fb62f35da74cfa839c23973ab08 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:01:15 +0100 Subject: [PATCH 146/238] Add license header to properties files --- sop-java/src/main/resources/sop-java-version.properties | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sop-java/src/main/resources/sop-java-version.properties b/sop-java/src/main/resources/sop-java-version.properties index 5f6f682..a2f509b 100644 --- a/sop-java/src/main/resources/sop-java-version.properties +++ b/sop-java/src/main/resources/sop-java-version.properties @@ -1 +1,4 @@ +# SPDX-FileCopyrightText: 2025 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 sop-java-version=@project.version@ \ No newline at end of file From f92a73a5ad46eb104929055e62317e088c3c0b38 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:01:46 +0100 Subject: [PATCH 147/238] Add back legacy --verify-out option alias for decrypt cmd --- .../src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt index 3f15f07..de98f17 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/DecryptCmd.kt @@ -29,7 +29,7 @@ class DecryptCmd : AbstractSopCmd() { @Option(names = [OPT_WITH_PASSWORD], paramLabel = "PASSWORD") var withPassword: List = listOf() - @Option(names = [OPT_VERIFICATIONS_OUT], paramLabel = "VERIFICATIONS") + @Option(names = [OPT_VERIFICATIONS_OUT, "--verify-out"], paramLabel = "VERIFICATIONS") var verifyOut: String? = null @Option(names = [OPT_VERIFY_WITH], paramLabel = "CERT") var certs: List = listOf() From 9ec3cc911bf1d69593231b1a1221362bec21297c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:12:04 +0100 Subject: [PATCH 148/238] Add bcsop reference in README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index baeb874..6be51d0 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ allowing to delegate the implementation logic to an arbitrary SOP CLI implementa |-------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| | [pgpainless-sop](https://github.com/pgpainless/pgpainless/tree/main/pgpainless-sop) | Implementation of `sop-java` using PGPainless | | [external-sop](https://github.com/pgpainless/sop-java/tree/main/external-sop) | Implementation of `sop-java` that allows binding to external SOP binaries such as `sqop` | +| [bcsop](https://codeberg.org/PGPainless/bc-sop) | Implementation of `sop-java` using vanilla Bouncy Castle | ### Implementations in other languages | Project | Language | From 690ba6dc16efa6ca60029620af3af0f28ac7efcf Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:12:17 +0100 Subject: [PATCH 149/238] Add rpgpie-sop reference in README --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6be51d0..e6ee9ab 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,8 @@ allowing to delegate the implementation logic to an arbitrary SOP CLI implementa | [bcsop](https://codeberg.org/PGPainless/bc-sop) | Implementation of `sop-java` using vanilla Bouncy Castle | ### Implementations in other languages -| Project | Language | -|-------------------------------------------------|----------| -| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust | -| [SOP for python](https://pypi.org/project/sop/) | Python | +| Project | Language | +|---------------------------------------------------|----------| +| [sop-rs](https://sequoia-pgp.gitlab.io/sop-rs/) | Rust | +| [SOP for python](https://pypi.org/project/sop/) | Python | +| [rpgpie-sop](https://crates.io/crates/rpgpie-sop) | Rust | From 97e91f50ab924dc42533491751a57e2640df62d1 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 28 Jan 2025 12:15:30 +0100 Subject: [PATCH 150/238] Migrate pipeline definition to use from_secret https://woodpecker-ci.org/docs/usage/secrets\#use-secrets-in-settings-and-environment --- .woodpecker/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index fab075a..2138cb4 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -16,4 +16,6 @@ steps: - gradle check javadocAll # Code has coverage - gradle jacocoRootReport coveralls - secrets: [coveralls_repo_token] + environment: + coveralls_repo_token: + from_secret: coveralls_repo_token From f2602bb413ba8b6e4f3edded7226814392252e25 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:04:50 +0100 Subject: [PATCH 151/238] Bump version to 10.1.0-SNAPSHOT --- .../src/main/kotlin/sop/operation/Version.kt | 16 ++++++++++++++++ version.gradle | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 5f26491..a10fe7c 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -4,6 +4,9 @@ package sop.operation +import java.io.IOException +import java.io.InputStream +import java.util.* import kotlin.jvm.Throws import sop.exception.SOPGPException @@ -107,4 +110,17 @@ interface Version { * this method throws an [SOPGPException.UnsupportedOption] instead. */ @Throws(SOPGPException.UnsupportedOption::class) fun getSopVVersion(): String + + /** Return the current version of the SOP-Java library. */ + fun getSopJavaVersion(): String? { + return try { + val resourceIn: InputStream = + javaClass.getResourceAsStream("/sop-java-version.properties") + ?: throw IOException("File sop-java-version.properties not found.") + val properties = Properties().apply { load(resourceIn) } + properties.getProperty("sop-java-version") + } catch (e: IOException) { + null + } + } } diff --git a/version.gradle b/version.gradle index 37c5577..3b0de55 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '10.0.4' + shortVersion = '10.1.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 From 924cfaa140b53c5652adacd0f2115bdd0a5b2544 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:18:24 +0100 Subject: [PATCH 152/238] Update README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e6ee9ab..f2e207f 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ The repository contains the following modules: compatible with the SOP-CLI specification. * [external-sop](/external-sop) contains an API implementation that can be used to forward API calls to a SOP executable, allowing to delegate the implementation logic to an arbitrary SOP CLI implementation. +* [sop-java-testfixtures](/sop-java-testfixtures) contains a test suite that can be shared by downstream implementations + of `sop-java`. ## Known Implementations (Please expand!) From c145f8bb37967722f821e2516f09945ea7a34ef7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:19:41 +0100 Subject: [PATCH 153/238] SOP-Java 10.1.0 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 367064c..49113f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.1.0-SNAPSHOT +## 10.1.0 - `sop-java`: - Remove `label()` option from `armor()` subcommand - Move test-fixtures artifact built with the `testFixtures` plugin into diff --git a/version.gradle b/version.gradle index 3b0de55..13c3b5f 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.1.0' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From d1893c5ea0ee239d3cef344922ce95c4d9fdb104 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 11 Mar 2025 21:22:31 +0100 Subject: [PATCH 154/238] SOP-Java 10.1.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 13c3b5f..33a2251 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.1.0' - isSnapshot = false + shortVersion = '10.1.1' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 1.8 gsonVersion = '2.10.1' From 51ba24ddbeaa8d5642fe8ac1bf90ac3a19553779 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 3 Apr 2025 14:18:46 +0200 Subject: [PATCH 155/238] Enable kapt annotation processing to properly embed picocli configuration files for native images into the -cli jar file For this it is apparently necessary to upgrade kotlin to 1.9.21 See https://stackoverflow.com/a/79030947/11150851 --- build.gradle | 3 ++- sop-java-picocli/build.gradle | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 577c2aa..78a7267 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { plugins { id 'ru.vyarus.animalsniffer' version '1.5.3' - id 'org.jetbrains.kotlin.jvm' version "1.8.10" + id 'org.jetbrains.kotlin.jvm' version "1.9.21" id 'com.diffplug.spotless' version '6.22.0' apply false } @@ -32,6 +32,7 @@ allprojects { apply plugin: 'jacoco' apply plugin: 'checkstyle' apply plugin: 'kotlin' + apply plugin: 'kotlin-kapt' apply plugin: 'com.diffplug.spotless' // For non-cli modules enable android api compatibility check diff --git a/sop-java-picocli/build.gradle b/sop-java-picocli/build.gradle index dbf0cc1..2203abe 100644 --- a/sop-java-picocli/build.gradle +++ b/sop-java-picocli/build.gradle @@ -21,7 +21,7 @@ dependencies { // CLI implementation "info.picocli:picocli:$picocliVersion" - annotationProcessor "info.picocli:picocli-codegen:$picocliVersion" + kapt "info.picocli:picocli-codegen:$picocliVersion" // @Nonnull, @Nullable... implementation "com.google.code.findbugs:jsr305:$jsrVersion" @@ -33,6 +33,10 @@ application { mainClass = mainClassName } +compileJava { + options.compilerArgs += ["-Aproject=${project.group}/${project.name}"] +} + jar { dependsOn(":sop-java:jar") duplicatesStrategy(DuplicatesStrategy.EXCLUDE) From 57e2f8391bed9935d4b4c469dbb2b2dafdb08ddb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Apr 2025 10:43:49 +0200 Subject: [PATCH 156/238] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49113f6..3028216 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 10.1.1-SNAPSHOT +- Prepare jar files for use in native images, e.g. using GraalVM by generating and including + configuration files for reflection, resources and dynamic proxies. + ## 10.1.0 - `sop-java`: - Remove `label()` option from `armor()` subcommand From edb405d79e2321c921b61b8ee350004c24817301 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Apr 2025 12:11:20 +0200 Subject: [PATCH 157/238] Add TODO to remove ProxyOutputStream in 11.X --- sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt index a608c89..4ba24b8 100644 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt @@ -16,6 +16,7 @@ import java.io.OutputStream * target output stream is not yet known. */ @Deprecated("Marked for removal.") +// TODO: Remove in 11.X class ProxyOutputStream : OutputStream() { private val buffer = ByteArrayOutputStream() private var swapped: OutputStream? = null From 859bb5bddecf9d13e7733da1f0eb9de0e6f8cafc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 4 Apr 2025 12:16:00 +0200 Subject: [PATCH 158/238] Fix issues in kdoc --- .../main/kotlin/sop/external/operation/EncryptExternal.kt | 2 +- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 +- sop-java/src/main/kotlin/sop/SigningResult.kt | 5 +++-- sop-java/src/main/kotlin/sop/operation/GenerateKey.kt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt index 12d9cff..679e09b 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/EncryptExternal.kt @@ -37,7 +37,7 @@ class EncryptExternal( override fun signWith(key: InputStream): Encrypt = apply { commandList.add("--sign-with=@ENV:SIGN_WITH_$argCounter") - envList.add("SIGN_WITH_$argCounter=${ExternalSOP.readString(key)}") + envList.add("SIGN_WITH_$argCounter=${readString(key)}") argCounter += 1 } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index b919370..62065f4 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -60,7 +60,7 @@ class SopCLI { @JvmField var EXECUTABLE_NAME = "sop" @JvmField - @Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + @Option(names = ["--stacktrace"], scope = ScopeType.INHERIT) var stacktrace = false @JvmStatic diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt index 29304ea..60888e0 100644 --- a/sop-java/src/main/kotlin/sop/SigningResult.kt +++ b/sop-java/src/main/kotlin/sop/SigningResult.kt @@ -9,8 +9,9 @@ package sop * * @param micAlg string identifying the digest mechanism used to create the signed message. This is * useful for setting the `micalg=` parameter for the multipart/signed content-type of a PGP/MIME - * object as described in section 5 of [RFC3156]. If more than one signature was generated and - * different digest mechanisms were used, the value of the micalg object is an empty string. + * object as described in section 5 of [RFC3156](https://www.rfc-editor.org/rfc/rfc3156#section-5). + * If more than one signature was generated and different digest mechanisms were used, the value + * of the micalg object is an empty string. */ data class SigningResult(val micAlg: MicAlg) { diff --git a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt index 3b83b99..13de39a 100644 --- a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt @@ -78,7 +78,7 @@ interface GenerateKey { fun signingOnly(): GenerateKey /** - * Generate the OpenPGP key and return it encoded as an [InputStream]. + * Generate the OpenPGP key and return it encoded as an [java.io.InputStream]. * * @return key * @throws MissingArg if no user-id was provided From 2c26ab2da524cbf28f705262e5b4f9952becff7f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:01:20 +0200 Subject: [PATCH 159/238] Improve reproducibility --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index 78a7267..4cc8dc4 100644 --- a/build.gradle +++ b/build.gradle @@ -78,6 +78,9 @@ allprojects { tasks.withType(AbstractArchiveTask) { preserveFileTimestamps = false reproducibleFileOrder = true + + dirMode = 0755 + fileMode = 0644 } // Compatibility of default implementations in kotlin interfaces with Java implementations. From 8394f2e5a8e40df8461a4845e09e921e0ab81c79 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:01:45 +0200 Subject: [PATCH 160/238] Make use of toolchain functionality and raise min Java API level to 11 --- build.gradle | 22 ++++++++++------------ version.gradle | 2 +- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/build.gradle b/build.gradle index 4cc8dc4..c73b60c 100644 --- a/build.gradle +++ b/build.gradle @@ -68,8 +68,6 @@ allprojects { description = "Stateless OpenPGP Protocol API for Java" version = shortVersion - sourceCompatibility = javaSourceCompatibility - repositories { mavenCentral() } @@ -83,6 +81,10 @@ allprojects { fileMode = 0644 } + kotlin { + jvmToolchain(javaSourceCompatibility) + } + // Compatibility of default implementations in kotlin interfaces with Java implementations. tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { @@ -115,7 +117,7 @@ allprojects { } jacoco { - toolVersion = "0.8.7" + toolVersion = "0.8.8" } jacocoTestReport { @@ -123,7 +125,7 @@ allprojects { sourceDirectories.setFrom(project.files(sourceSets.main.allSource.srcDirs)) classDirectories.setFrom(project.files(sourceSets.main.output)) reports { - xml.enabled true + xml.required = true } } @@ -141,15 +143,15 @@ subprojects { apply plugin: 'signing' task sourcesJar(type: Jar, dependsOn: classes) { - classifier = 'sources' + archiveClassifier = 'sources' from sourceSets.main.allSource } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } task testsJar(type: Jar, dependsOn: testClasses) { - classifier = 'tests' + archiveClassifier = 'tests' from sourceSets.test.output } @@ -246,7 +248,7 @@ task jacocoRootReport(type: JacocoReport) { classDirectories.setFrom(files(subprojects.sourceSets.main.output)) executionData.setFrom(files(subprojects.jacocoTestReport.executionData)) reports { - xml.enabled true + xml.required = true xml.destination file("${buildDir}/reports/jacoco/test/jacocoTestReport.xml") } // We could remove the following setOnlyIf line, but then @@ -257,10 +259,6 @@ task jacocoRootReport(type: JacocoReport) { } task javadocAll(type: Javadoc) { - def currentJavaVersion = JavaVersion.current() - if (currentJavaVersion.compareTo(JavaVersion.VERSION_1_9) >= 0) { - options.addStringOption("-release", "8"); - } source subprojects.collect {project -> project.sourceSets.main.allJava } destinationDir = new File(buildDir, 'javadoc') diff --git a/version.gradle b/version.gradle index 33a2251..9619f9a 100644 --- a/version.gradle +++ b/version.gradle @@ -7,7 +7,7 @@ allprojects { shortVersion = '10.1.1' isSnapshot = true minAndroidSdk = 10 - javaSourceCompatibility = 1.8 + javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' junitVersion = '5.8.2' From cddc92bd92ef5354c10a5236fe8ba75e890c5096 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:13:40 +0200 Subject: [PATCH 161/238] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3028216..1746078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ SPDX-License-Identifier: Apache-2.0 ## 10.1.1-SNAPSHOT - Prepare jar files for use in native images, e.g. using GraalVM by generating and including configuration files for reflection, resources and dynamic proxies. +- gradle: Make use of jvmToolchain functionality +- gradle: Improve reproducibility ## 10.1.0 - `sop-java`: From a2a3bda2b3c321c1561d217cb2f5cae7f5f6a5c8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:26:09 +0200 Subject: [PATCH 162/238] Migrate AbortOnUnsupportedOption annotation back to java --- .../testsuite/AbortOnUnsupportedOption.java | 18 +++++++++++++ .../AbortOnUnsupportedOptionExtension.java | 26 +++++++++++++++++++ .../sop/testsuite/AbortOnUnsupportedOption.kt | 12 --------- .../AbortOnUnsupportedOptionExtension.kt | 21 --------------- 4 files changed, 44 insertions(+), 33 deletions(-) create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java delete mode 100644 sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt delete mode 100644 sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java new file mode 100644 index 0000000..cbd0746 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOption.java @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +public @interface AbortOnUnsupportedOption { + +} diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java new file mode 100644 index 0000000..0bf366d --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/AbortOnUnsupportedOptionExtension.java @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; +import sop.exception.SOPGPException; + +import java.lang.annotation.Annotation; + +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +public class AbortOnUnsupportedOptionExtension implements TestExecutionExceptionHandler { + + @Override + public void handleTestExecutionException(ExtensionContext extensionContext, Throwable throwable) throws Throwable { + Class testClass = extensionContext.getRequiredTestClass(); + Annotation annotation = testClass.getAnnotation(AbortOnUnsupportedOption.class); + if (annotation != null && throwable instanceof SOPGPException.UnsupportedOption) { + assumeTrue(false, "Test aborted due to: " + throwable.getMessage()); + } + throw throwable; + } +} diff --git a/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt deleted file mode 100644 index cf99671..0000000 --- a/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOption.kt +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite - -import java.lang.annotation.Inherited - -@Target(AnnotationTarget.TYPE) -@Retention(AnnotationRetention.RUNTIME) -@Inherited -annotation class AbortOnUnsupportedOption diff --git a/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt b/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt deleted file mode 100644 index 809c78f..0000000 --- a/sop-java-testfixtures/src/main/kotlin/sop/testsuite/AbortOnUnsupportedOptionExtension.kt +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-FileCopyrightText: 2024 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite - -import org.junit.jupiter.api.Assumptions -import org.junit.jupiter.api.extension.ExtensionContext -import org.junit.jupiter.api.extension.TestExecutionExceptionHandler -import sop.exception.SOPGPException - -class AbortOnUnsupportedOptionExtension : TestExecutionExceptionHandler { - override fun handleTestExecutionException(context: ExtensionContext, throwable: Throwable) { - val testClass = context.requiredTestClass - val annotation = testClass.getAnnotation(AbortOnUnsupportedOption::class.java) - if (annotation != null && SOPGPException.UnsupportedOption::class.isInstance(throwable)) { - Assumptions.assumeTrue(false, "Test aborted due to: " + throwable.message) - } - throw throwable - } -} From e3fe9410d7dbd30c08e86810758fb47a20c71970 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:27:54 +0200 Subject: [PATCH 163/238] reuse: Migrate to toml format --- .reuse/dep5 | 29 ----------------------------- REUSE.toml | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 29 deletions(-) delete mode 100644 .reuse/dep5 create mode 100644 REUSE.toml diff --git a/.reuse/dep5 b/.reuse/dep5 deleted file mode 100644 index f43556c..0000000 --- a/.reuse/dep5 +++ /dev/null @@ -1,29 +0,0 @@ -Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: SOP-Java -Upstream-Contact: Paul Schaub -Source: https://pgpainless.org - -# Sample paragraph, commented out: -# -# Files: src/* -# Copyright: $YEAR $NAME <$CONTACT> -# License: ... - -# Gradle build tool -Files: gradle* -Copyright: 2015 the original author or authors. -License: Apache-2.0 - -# Woodpecker build files -Files: .woodpecker/* -Copyright: 2022 the original author or authors. -License: Apache-2.0 - -Files: external-sop/src/main/resources/sop/testsuite/external/* -Copyright: 2023 the original author or authors -License: Apache-2.0 - -# Github Issue Templates -Files: .github/ISSUE_TEMPLATE/* -Copyright: 2024 the original author or authors -License: Apache-2.0 diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..f82d8f8 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: 2025 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 + +version = 1 +SPDX-PackageName = "SOP-Java" +SPDX-PackageSupplier = "Paul Schaub " +SPDX-PackageDownloadLocation = "https://pgpainless.org" + +[[annotations]] +path = "gradle**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2015 the original author or authors." +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".woodpecker/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2022 the original author or authors." +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = "external-sop/src/main/resources/sop/testsuite/external/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2023 the original author or authors" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ".github/ISSUE_TEMPLATE/**" +precedence = "aggregate" +SPDX-FileCopyrightText = "2024 the original author or authors" +SPDX-License-Identifier = "Apache-2.0" From 4d2876a296539ec1e867d33c7228e105c9f390e0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:28:14 +0200 Subject: [PATCH 164/238] Fix formatting issue --- sop-java/src/main/kotlin/sop/SigningResult.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/SigningResult.kt b/sop-java/src/main/kotlin/sop/SigningResult.kt index 60888e0..651f8c1 100644 --- a/sop-java/src/main/kotlin/sop/SigningResult.kt +++ b/sop-java/src/main/kotlin/sop/SigningResult.kt @@ -9,9 +9,10 @@ package sop * * @param micAlg string identifying the digest mechanism used to create the signed message. This is * useful for setting the `micalg=` parameter for the multipart/signed content-type of a PGP/MIME - * object as described in section 5 of [RFC3156](https://www.rfc-editor.org/rfc/rfc3156#section-5). - * If more than one signature was generated and different digest mechanisms were used, the value - * of the micalg object is an empty string. + * object as described in section 5 of + * [RFC3156](https://www.rfc-editor.org/rfc/rfc3156#section-5). If more than one signature was + * generated and different digest mechanisms were used, the value of the micalg object is an empty + * string. */ data class SigningResult(val micAlg: MicAlg) { From 2d99aea4ab7ec2fa1e32f309fc21f7f7fcf0e492 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:29:04 +0200 Subject: [PATCH 165/238] Bump animalsniffer to 2.0.0 --- CHANGELOG.md | 1 + build.gradle | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1746078..b2f0a18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ SPDX-License-Identifier: Apache-2.0 configuration files for reflection, resources and dynamic proxies. - gradle: Make use of jvmToolchain functionality - gradle: Improve reproducibility +- gradle: Bump animalsniffer to `2.0.0` ## 10.1.0 - `sop-java`: diff --git a/build.gradle b/build.gradle index c73b60c..1bff704 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ buildscript { } plugins { - id 'ru.vyarus.animalsniffer' version '1.5.3' + id 'ru.vyarus.animalsniffer' version '2.0.0' id 'org.jetbrains.kotlin.jvm' version "1.9.21" id 'com.diffplug.spotless' version '6.22.0' apply false } From 701f9453ca0bb4125a07a91d3b6313a9f5d6df80 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:41:50 +0200 Subject: [PATCH 166/238] SOP-Java 10.1.1 --- CHANGELOG.md | 2 +- version.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f0a18..c0500cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 10.1.1-SNAPSHOT +## 10.1.1 - Prepare jar files for use in native images, e.g. using GraalVM by generating and including configuration files for reflection, resources and dynamic proxies. - gradle: Make use of jvmToolchain functionality diff --git a/version.gradle b/version.gradle index 9619f9a..5fdbcad 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '10.1.1' - isSnapshot = true + isSnapshot = false minAndroidSdk = 10 javaSourceCompatibility = 11 gsonVersion = '2.10.1' From cbeec9c90d68ff7ebdcb8ca146ae4481ba2b787f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 11:44:22 +0200 Subject: [PATCH 167/238] SOP-Java 10.1.2-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index 5fdbcad..b4908ee 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '10.1.1' - isSnapshot = false + shortVersion = '10.1.2' + isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 11 gsonVersion = '2.10.1' From ad137d63514822945cb94a0487f36b0dc5eb91b8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 May 2025 02:16:52 +0200 Subject: [PATCH 168/238] Try to fix coveralls repo token --- .woodpecker/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.woodpecker/build.yml b/.woodpecker/build.yml index 2138cb4..4c23ffb 100644 --- a/.woodpecker/build.yml +++ b/.woodpecker/build.yml @@ -17,5 +17,5 @@ steps: # Code has coverage - gradle jacocoRootReport coveralls environment: - coveralls_repo_token: + COVERALLS_REPO_TOKEN: from_secret: coveralls_repo_token From 1dcf13244d1a0e4ac668f01a1d10dc6f3f4294ec Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:40:36 +0200 Subject: [PATCH 169/238] Add new exceptions --- .../src/main/resources/msg_sop.properties | 2 ++ .../src/main/resources/msg_sop_de.properties | 2 ++ .../kotlin/sop/exception/SOPGPException.kt | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 94e4dc0..8179676 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -38,6 +38,8 @@ usage.exitCodeList.19=83:Options were supplied that are incompatible with each o 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 stacktrace=Print stacktrace diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 786fa36..0538cd9 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -38,6 +38,8 @@ 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.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 stacktrace=Stacktrace ausgeben diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index bc9131f..1f9ce6b 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -337,4 +337,34 @@ abstract class SOPGPException : RuntimeException { 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 { + constructor() : super() + + constructor(errorMsg: String) : super(errorMsg) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 107 + } + } } From 4115a5041d0c49242f61231e37c283863e862df0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:36 +0200 Subject: [PATCH 170/238] Add implementation of update-key command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../sop/cli/picocli/commands/UpdateKeyCmd.kt | 76 +++++++++++++++++++ .../main/resources/msg_update-key.properties | 19 +++++ .../resources/msg_update-key_de.properties | 18 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 18 ++--- .../main/kotlin/sop/operation/UpdateKey.kt | 43 +++++++++++ 6 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_update-key.properties create mode 100644 sop-java-picocli/src/main/resources/msg_update-key_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/UpdateKey.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 62065f4..df8e883 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -27,6 +27,7 @@ import sop.exception.SOPGPException ChangeKeyPasswordCmd::class, RevokeKeyCmd::class, ExtractCertCmd::class, + UpdateKeyCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt new file mode 100644 index 0000000..2afa015 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine.Command +import picocli.CommandLine.Option +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException.* +import java.io.IOException + +@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 = listOf() + + @Option(names = ["--merge-certs"], paramLabel = "CERTS") + var mergeCerts: List = 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 { certIn -> updateKey.mergeCerts(certIn) } + } 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) + } + } +} \ No newline at end of file diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties new file mode 100644 index 0000000..dd4446d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# 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 + +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 diff --git a/sop-java-picocli/src/main/resources/msg_update-key_de.properties b/sop-java-picocli/src/main/resources/msg_update-key_de.properties new file mode 100644 index 0000000..86b999e --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# 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 + +# 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 diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 7fdd414..c53bb7d 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -4,18 +4,7 @@ package sop -import sop.operation.Armor -import sop.operation.ChangeKeyPassword -import sop.operation.Dearmor -import sop.operation.Decrypt -import sop.operation.DetachedSign -import sop.operation.Encrypt -import sop.operation.ExtractCert -import sop.operation.GenerateKey -import sop.operation.InlineDetach -import sop.operation.InlineSign -import sop.operation.ListProfiles -import sop.operation.RevokeKey +import sop.operation.* /** * Stateless OpenPGP Interface. This class provides a stateless interface to various OpenPGP related @@ -70,4 +59,9 @@ interface SOP : SOPV { /** Update a key's password. */ fun changeKeyPassword(): ChangeKeyPassword + + /** + * Keep a secret key up-to-date. + */ + fun updateKey(): UpdateKey } diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt new file mode 100644 index 0000000..1b12f6f --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -0,0 +1,43 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import sop.util.UTF8Util +import java.io.IOException +import java.io.InputStream + +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()) +} \ No newline at end of file From 84404d629fa91a5d70399a9043486093ee7632c2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Sep 2024 22:43:50 +0200 Subject: [PATCH 171/238] Add implementation of merge-certs command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../sop/cli/picocli/commands/MergeCertsCmd.kt | 48 +++++++++++++++++++ .../main/resources/msg_merge-certs.properties | 15 ++++++ .../resources/msg_merge-certs_de.properties | 19 ++++++++ sop-java/src/main/kotlin/sop/SOP.kt | 5 ++ .../main/kotlin/sop/operation/MergeCerts.kt | 28 +++++++++++ 6 files changed, 116 insertions(+) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_merge-certs.properties create mode 100644 sop-java-picocli/src/main/resources/msg_merge-certs_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/MergeCerts.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index df8e883..9d1a305 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -28,6 +28,7 @@ import sop.exception.SOPGPException RevokeKeyCmd::class, ExtractCertCmd::class, UpdateKeyCmd::class, + MergeCertsCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt new file mode 100644 index 0000000..15b33f8 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import picocli.CommandLine +import picocli.CommandLine.Command +import sop.cli.picocli.SopCLI +import sop.exception.SOPGPException +import java.io.IOException + +@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 = 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) + } + } +} \ No newline at end of file diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties new file mode 100644 index 0000000..866db4b --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.headerHeading=Merge OpenPGP certificates%n +usage.description=BLABLA +no-armor=ASCII armor the output +CERTS[0..*]=OpenPGP certificates from which updates shall be merged into the base certificates from standard input + +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 diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties new file mode 100644 index 0000000..021c535 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# SPDX-License-Identifier: Apache-2.0 +usage.headerHeading=OpenPGP Zertifikate zusammenführen%n%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 + +usage.parameterList.0=STANDARDIN +usage.parameterList.1=STANDARDOUT + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=%nParameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.descriptionHeading=%nHinweise:%n +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading = %nOptionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index c53bb7d..1640d5f 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -64,4 +64,9 @@ interface SOP : SOPV { * Keep a secret key up-to-date. */ fun updateKey(): UpdateKey + + /** + * Merge OpenPGP certificates. + */ + fun mergeCerts(): MergeCerts } diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt new file mode 100644 index 0000000..f60d291 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import java.io.IOException +import java.io.InputStream + +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()) +} \ No newline at end of file From ada77be955720dfca424c27b763878e453b2b65b Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 15:50:17 +0200 Subject: [PATCH 172/238] Add support for rendering help info for input and output --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 8 +- .../cli/picocli/commands/AbstractSopCmd.kt | 100 ++++++++++++++++++ .../src/main/resources/msg_sop.properties | 7 +- .../src/main/resources/msg_sop_de.properties | 5 +- 4 files changed, 115 insertions(+), 5 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 9d1a305..4db6b30 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -83,16 +83,20 @@ class SopCLI { return CommandLine(SopCLI::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) + // render Input/Output sections in help command + subcommands.values.filter { (it.getCommand() as Any) is AbstractSopCmd } // Only for AbstractSopCmd objects + .forEach { (it.getCommand() as AbstractSopCmd).installIORenderer(it) } // overwrite executable name commandName = EXECUTABLE_NAME // setup exception handling executionExceptionHandler = SOPExecutionExceptionHandler() exitCodeExceptionMapper = SOPExceptionExitCodeMapper() isCaseInsensitiveEnumValuesAllowed = true - } - .execute(*args) + }.execute(*args) } } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt index 4629e57..65be1be 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/AbstractSopCmd.kt @@ -7,6 +7,11 @@ package sop.cli.picocli.commands import java.io.* import java.text.ParseException import java.util.* +import picocli.CommandLine +import picocli.CommandLine.Help +import picocli.CommandLine.Help.Column +import picocli.CommandLine.Help.TextTable +import picocli.CommandLine.IHelpSectionRenderer import sop.cli.picocli.commands.AbstractSopCmd.EnvironmentVariableResolver import sop.exception.SOPGPException.* import sop.util.UTCUtil.Companion.parseUTCDate @@ -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) : + 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, header: String, details: String): List { + val index = + keys.indexOf(CommandLine.Model.UsageMessageSpec.SECTION_KEY_EXIT_CODE_LIST_HEADING) + val result = keys.toMutableList() + result.add(index, header) + result.add(index + 1, details) + return result + } + + private fun getResString(cmd: CommandLine, key: String): String? = + try { + cmd.resourceBundle.getString(key) + } catch (m: MissingResourceException) { + try { + cmd.parent.resourceBundle.getString(key) + } catch (m: MissingResourceException) { + null + } + } + ?.let { String.format(it) } + companion object { const val PRFX_ENV = "@ENV:" const val PRFX_FD = "@FD:" + const val SECTION_KEY_STANDARD_INPUT_HEADING = "standardInputHeading" + const val SECTION_KEY_STANDARD_INPUT_DETAILS = "standardInput" + const val SECTION_KEY_STANDARD_OUTPUT_HEADING = "standardOutputHeading" + const val SECTION_KEY_STANDARD_OUTPUT_DETAILS = "standardOutput" + @JvmField val DAWN_OF_TIME = Date(0) @JvmField diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 8179676..d5d997a 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -9,10 +9,13 @@ locale=Locale for description texts # Generic usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n +standardInputHeading=%nInput:%n +standardOutputHeading=%nOutput:%n + # Exit Codes usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeList.0=\u00200:Successful program execution diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 0538cd9..73efe89 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -10,9 +10,12 @@ locale=Gebietsschema f # Generic usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n +standardInputHeading=%nEingabe:%n +standardOutputHeading=%nAusgabe:%n + # Exit Codes usage.exitCodeListHeading=%nExit Codes:%n usage.exitCodeList.0=\u00200:Erfolgreiche Programmausführung From d6c133087435fa78f0ae246bbf8f4d8c2a388295 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:30 +0200 Subject: [PATCH 173/238] Implement certify-userid command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../cli/picocli/commands/CertifyUserIdCmd.kt | 87 +++++++++++++++++++ .../resources/msg_certify-userid.properties | 23 +++++ .../msg_certify-userid_de.properties | 20 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 5 ++ .../kotlin/sop/operation/CertifyUserId.kt | 41 +++++++++ 6 files changed, 177 insertions(+) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_certify-userid.properties create mode 100644 sop-java-picocli/src/main/resources/msg_certify-userid_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 4db6b30..d234e54 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -29,6 +29,7 @@ import sop.exception.SOPGPException ExtractCertCmd::class, UpdateKeyCmd::class, MergeCertsCmd::class, + CertifyUserIdCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt new file mode 100644 index 0000000..71ef79f --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli.commands + +import java.io.IOException +import picocli.CommandLine.Command +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Option +import picocli.CommandLine.Parameters +import picocli.CommandLine.Spec +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) +class CertifyUserIdCmd : AbstractSopCmd() { + + @Spec var spec: CommandSpec? = null + + @Option(names = ["--no-armor"], negatable = true) var armor = true + + @Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID") + var userIds: List = listOf() + + @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") + var withKeyPassword: List = listOf() + + @Option(names = ["--no-require-self-sig"]) var noRequireSelfSig = false + + @Parameters(paramLabel = "KEYS", arity = "1..*") var keys: List = 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) + } + } +} diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties new file mode 100644 index 0000000..5eb7aa3 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# 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 + +stacktrace=Print stacktrace +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=Parameters:%n +usage.synopsisHeading=Usage:\u0020 +usage.commandListHeading = Commands:%n +usage.optionListHeading = Options:%n +usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties new file mode 100644 index 0000000..0237fa6 --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# 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 + +# Generic TODO: Remove when bumping picocli to 4.7.0 +usage.parameterListHeading=Parameter:%n +usage.synopsisHeading=Aufruf:\u0020 +usage.commandListHeading=Befehle:%n +usage.optionListHeading = Optionen:%n +usage.footerHeading=Powered by Picocli%n diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 1640d5f..5435cad 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -69,4 +69,9 @@ interface SOP : SOPV { * Merge OpenPGP certificates. */ fun mergeCerts(): MergeCerts + + /** + * Certify OpenPGP Certificate User-IDs. + */ + fun certifyUserId(): CertifyUserId } diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt new file mode 100644 index 0000000..92fff20 --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.operation + +import sop.Ready +import sop.exception.SOPGPException +import sop.util.UTF8Util +import java.io.IOException +import java.io.InputStream + +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()) +} \ No newline at end of file From 2b6a5dd651d0063be1558e1de7ae357a83eff0af Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:01:58 +0200 Subject: [PATCH 174/238] Checkstyle and exception handling improvements --- .../sop/cli/picocli/commands/MergeCertsCmd.kt | 10 ++++------ .../sop/cli/picocli/commands/UpdateKeyCmd.kt | 15 +++++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt index 15b33f8..16d56e3 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -4,11 +4,11 @@ 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 -import java.io.IOException @Command( name = "merge-certs", @@ -16,11 +16,9 @@ import java.io.IOException exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) class MergeCertsCmd : AbstractSopCmd() { - @CommandLine.Option(names = ["--no-armor"], negatable = true) - var armor = false + @CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = false - @CommandLine.Parameters(paramLabel = "CERTS") - var updates: List = listOf() + @CommandLine.Parameters(paramLabel = "CERTS") var updates: List = listOf() override fun run() { val mergeCerts = throwIfUnsupportedSubcommand(SopCLI.getSop().mergeCerts(), "merge-certs") @@ -45,4 +43,4 @@ class MergeCertsCmd : AbstractSopCmd() { throw RuntimeException(e) } } -} \ No newline at end of file +} diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt index 2afa015..08f9297 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -4,11 +4,11 @@ package sop.cli.picocli.commands +import java.io.IOException import picocli.CommandLine.Command import picocli.CommandLine.Option import sop.cli.picocli.SopCLI import sop.exception.SOPGPException.* -import java.io.IOException @Command( name = "update-key", @@ -25,8 +25,7 @@ class UpdateKeyCmd : AbstractSopCmd() { @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") var withKeyPassword: List = listOf() - @Option(names = ["--merge-certs"], paramLabel = "CERTS") - var mergeCerts: List = listOf() + @Option(names = ["--merge-certs"], paramLabel = "CERTS") var mergeCerts: List = listOf() override fun run() { val updateKey = throwIfUnsupportedSubcommand(SopCLI.getSop().updateKey(), "update-key") @@ -48,7 +47,8 @@ class UpdateKeyCmd : AbstractSopCmd() { val password = stringFromInputStream(getInput(passwordFileName)) updateKey.withKeyPassword(password) } catch (unsupportedOption: UnsupportedOption) { - val errorMsg = getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") + val errorMsg = + getMsg("sop.error.feature_support.option_not_supported", "--with-key-password") throw UnsupportedOption(errorMsg, unsupportedOption) } catch (e: IOException) { throw RuntimeException(e) @@ -57,7 +57,7 @@ class UpdateKeyCmd : AbstractSopCmd() { for (certInput in mergeCerts) { try { - getInput(certInput).use { certIn -> updateKey.mergeCerts(certIn) } + getInput(certInput).use { updateKey.mergeCerts(it) } } catch (e: IOException) { throw RuntimeException(e) } catch (badData: BadData) { @@ -71,6 +71,9 @@ class UpdateKeyCmd : AbstractSopCmd() { 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) } } -} \ No newline at end of file +} From 122cd016a1330bf6b723e84fc829da990d8de36f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 18 Sep 2024 16:56:26 +0200 Subject: [PATCH 175/238] Update msg files with input/output information --- .../src/main/resources/msg_armor.properties | 8 ++++++-- .../src/main/resources/msg_armor_de.properties | 6 ++++-- .../src/main/resources/msg_certify-userid.properties | 6 +++--- .../main/resources/msg_certify-userid_de.properties | 6 +++--- .../main/resources/msg_change-key-password.properties | 9 +++++++-- .../resources/msg_change-key-password_de.properties | 5 ++++- .../src/main/resources/msg_dearmor.properties | 9 +++++++-- .../src/main/resources/msg_dearmor_de.properties | 5 ++++- .../src/main/resources/msg_decrypt.properties | 9 +++++++-- .../src/main/resources/msg_decrypt_de.properties | 5 ++++- .../src/main/resources/msg_detached-sign.properties | 9 +++++++-- .../main/resources/msg_detached-sign_de.properties | 5 ++++- .../src/main/resources/msg_detached-verify.properties | 9 +++++++-- .../main/resources/msg_detached-verify_de.properties | 5 ++++- .../src/main/resources/msg_encrypt.properties | 9 +++++++-- .../src/main/resources/msg_encrypt_de.properties | 5 ++++- .../src/main/resources/msg_extract-cert.properties | 9 +++++++-- .../src/main/resources/msg_extract-cert_de.properties | 5 ++++- .../src/main/resources/msg_generate-key.properties | 7 +++++-- .../src/main/resources/msg_generate-key_de.properties | 4 +++- .../src/main/resources/msg_help.properties | 4 ++-- .../src/main/resources/msg_help_de.properties | 2 +- .../src/main/resources/msg_inline-detach.properties | 9 +++++++-- .../main/resources/msg_inline-detach_de.properties | 5 ++++- .../src/main/resources/msg_inline-sign.properties | 9 +++++++-- .../src/main/resources/msg_inline-sign_de.properties | 5 ++++- .../src/main/resources/msg_inline-verify.properties | 9 +++++++-- .../main/resources/msg_inline-verify_de.properties | 5 ++++- .../src/main/resources/msg_list-profiles.properties | 7 +++++-- .../main/resources/msg_list-profiles_de.properties | 4 +++- .../src/main/resources/msg_merge-certs.properties | 10 ++++++++-- .../src/main/resources/msg_merge-certs_de.properties | 10 +++++----- .../src/main/resources/msg_revoke-key.properties | 11 ++++++++--- .../src/main/resources/msg_revoke-key_de.properties | 5 ++++- .../src/main/resources/msg_sop.properties | 1 + .../src/main/resources/msg_sop_de.properties | 1 + .../src/main/resources/msg_update-key.properties | 9 +++++++-- .../src/main/resources/msg_update-key_de.properties | 5 ++++- .../src/main/resources/msg_version.properties | 6 ++++-- .../src/main/resources/msg_version_de.properties | 4 +++- 40 files changed, 190 insertions(+), 66 deletions(-) diff --git a/sop-java-picocli/src/main/resources/msg_armor.properties b/sop-java-picocli/src/main/resources/msg_armor.properties index b4dcb59..1b7c1fb 100644 --- a/sop-java-picocli/src/main/resources/msg_armor.properties +++ b/sop-java-picocli/src/main/resources/msg_armor.properties @@ -3,9 +3,13 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Add ASCII Armor to standard input +standardInput=BINARY +standardInputDescription=OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutput=ARMORED +standardOutputDescription=Same material, but with ASCII-armoring added, if not already present + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_armor_de.properties b/sop-java-picocli/src/main/resources/msg_armor_de.properties index 4c365a8..34383c8 100644 --- a/sop-java-picocli/src/main/resources/msg_armor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_armor_de.properties @@ -3,9 +3,11 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Schütze Standard-Eingabe mit ASCII Armor +standardInputDescription=OpenPGP Material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutputDescription=Dasselbe Material, aber mit ASCII Armor kodiert, falls noch nicht geschehen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 -usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties index 5eb7aa3..252aae4 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -16,8 +16,8 @@ standardOutputDescription=Certified certificates stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.parameterListHeading=Parameters:%n +usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = Commands:%n -usage.optionListHeading = Options:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties index 0237fa6..9f0a673 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -13,8 +13,8 @@ standardInputDescription=Zertifikate, auf denen Identit standardOutputDescription=Zertifizierte Zertifikate # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.parameterListHeading=Parameter:%n +usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 -usage.commandListHeading=Befehle:%n -usage.optionListHeading = Optionen:%n +usage.commandListHeading=%nBefehle:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_change-key-password.properties b/sop-java-picocli/src/main/resources/msg_change-key-password.properties index 3de3608..79bc11b 100644 --- a/sop-java-picocli/src/main/resources/msg_change-key-password.properties +++ b/sop-java-picocli/src/main/resources/msg_change-key-password.properties @@ -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.2=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardInput=KEYS +standardInputDescription=OpenPGP keys whose passphrases shall be changed +standardOutput=KEYS +standardOutputDescription=OpenPGP keys with changed passphrases + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties index 014c3e7..5515c1d 100644 --- a/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties +++ b/sop-java-picocli/src/main/resources/msg_change-key-password_de.properties @@ -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.2=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardInputDescription=OpenPGP Schlüssel deren Passwörter geändert werden sollen +standardOutputDescription=OpenPGP Schlüssel mit geänderten Passwörtern + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_dearmor.properties b/sop-java-picocli/src/main/resources/msg_dearmor.properties index b088de1..55cbf45 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor.properties @@ -3,9 +3,14 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Remove ASCII Armor from standard input +standardInput=ARMORED +standardInputDescription=Armored OpenPGP material (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutput=BINARY +standardOutputDescription=Same material, but with ASCII-armoring removed + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_dearmor_de.properties b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties index 362ccef..e01ab7a 100644 --- a/sop-java-picocli/src/main/resources/msg_dearmor_de.properties +++ b/sop-java-picocli/src/main/resources/msg_dearmor_de.properties @@ -3,9 +3,12 @@ # SPDX-License-Identifier: Apache-2.0 usage.header=Entferne ASCII Armor von Standard-Eingabe +standardInputDescription=OpenPGP Material mit ASCII Armor (SIGNATURES, KEYS, CERTS, CIPHERTEXT, INLINESIGNED) +standardOutputDescription=Dasselbe Material, aber mit entfernter ASCII Armor + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_decrypt.properties b/sop-java-picocli/src/main/resources/msg_decrypt.properties index 5903ded..bec315f 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt.properties @@ -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...). KEY[0..*]=Secret keys to attempt decryption with +standardInput=CIPHERTEXT +standardInputDescription=Encrypted OpenPGP message +standardOutput=DATA +standardOutputDescription=Decrypted OpenPGP message + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties index ba40897..395a89f 100644 --- a/sop-java-picocli/src/main/resources/msg_decrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_decrypt_de.properties @@ -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...). KEY[0..*]=Private Schlüssel zum Entschlüsseln der Nachricht +standardInputDescription=Verschlüsselte OpenPGP Nachricht +standardOutputDescription=Entschlüsselte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-sign.properties b/sop-java-picocli/src/main/resources/msg_detached-sign.properties index 83359a6..6ebfd0b 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign.properties @@ -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). 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 # 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.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties b/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties index b943da5..39b59b5 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-sign_de.properties @@ -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. KEYS[0..*]=Private Signaturschlüssel +standardInputDescription=Daten die signiert werden sollen +standardOutputDescription=Abgetrennte OpenPGP Signatur(en) + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-verify.properties b/sop-java-picocli/src/main/resources/msg_detached-verify.properties index ee1a468..074a318 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify.properties @@ -13,11 +13,16 @@ not-after.3=Accepts special value "-" for end of time. SIGNATURE[0]=Detached signature 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 # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.parameterListHeading=%nParameters:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties b/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties index 332bff6..e21ee2a 100644 --- a/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties +++ b/sop-java-picocli/src/main/resources/msg_detached-verify_de.properties @@ -13,11 +13,14 @@ not-after.3=Akzeptiert speziellen Wert '-' f SIGNATURE[0]=Abgetrennte Signatur 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 # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_encrypt.properties b/sop-java-picocli/src/main/resources/msg_encrypt.properties index c0f7f7d..7bbf593 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt.properties @@ -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...). 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 # 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.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties index 6a3055c..55b0338 100644 --- a/sop-java-picocli/src/main/resources/msg_encrypt_de.properties +++ b/sop-java-picocli/src/main/resources/msg_encrypt_de.properties @@ -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...). 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 # 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.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_extract-cert.properties b/sop-java-picocli/src/main/resources/msg_extract-cert.properties index 82cac0f..1d1dee4 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert.properties @@ -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. 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 # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nDescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties b/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties index 0946cfc..c92d31d 100644 --- a/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties +++ b/sop-java-picocli/src/main/resources/msg_extract-cert_de.properties @@ -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. no-armor=Schütze Ausgabe mit ASCII Armor +standardInputDescription=Private Schlüssel, deren Zertifikate extrahiert werden sollen +standardOutputDescription=Extrahierte Zertifikate + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_generate-key.properties b/sop-java-picocli/src/main/resources/msg_generate-key.properties index 60ff4a4..c17f7f6 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key.properties @@ -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.1=Is an INDIRECT data type (e.g. file, environment variable, file descriptor...). +standardOutput=KEYS +standardOutputDescription=Generated 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.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_generate-key_de.properties b/sop-java-picocli/src/main/resources/msg_generate-key_de.properties index 6a0ce13..84db04d 100644 --- a/sop-java-picocli/src/main/resources/msg_generate-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_generate-key_de.properties @@ -9,10 +9,12 @@ signing-only=Generiere einen Schl with-key-password.0=Passwort zum Schutz des privaten Schlüssels with-key-password.1=Ist ein INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dateideskriptor...). +standardOutputDescription=Erzeugter OpenPGP Schlüssel + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_help.properties b/sop-java-picocli/src/main/resources/msg_help.properties index 797cc79..637c1d0 100644 --- a/sop-java-picocli/src/main/resources/msg_help.properties +++ b/sop-java-picocli/src/main/resources/msg_help.properties @@ -6,6 +6,6 @@ usage.header=Display usage information for the specified subcommand stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_help_de.properties b/sop-java-picocli/src/main/resources/msg_help_de.properties index beea45c..8471188 100644 --- a/sop-java-picocli/src/main/resources/msg_help_de.properties +++ b/sop-java-picocli/src/main/resources/msg_help_de.properties @@ -7,5 +7,5 @@ stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-detach.properties b/sop-java-picocli/src/main/resources/msg_inline-detach.properties index c100c51..ca0ed6b 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-detach.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-detach.properties @@ -5,9 +5,14 @@ usage.header=Split signatures from a clearsigned message no-armor=ASCII armor the output 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 # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties b/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties index e59aa34..84b8c47 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-detach_de.properties @@ -5,9 +5,12 @@ usage.header=Trenne Signaturen von Klartext-signierter Nachricht no-armor=Schütze Ausgabe mit ASCII Armor signatures-out=Schreibe abgetrennte Signaturen in Ausgabe +standardInputDescription=Klartext-signierte OpenPGP Nachricht +standardOutputDescription=Nachricht ohne Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-sign.properties b/sop-java-picocli/src/main/resources/msg_inline-sign.properties index f5143bb..936b417 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign.properties @@ -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). 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 # 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.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties b/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties index b09b7e4..f8fe906 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-sign_de.properties @@ -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. KEYS[0..*]=Private Signaturschlüssel +standardInputDescription=Daten, die signiert werden sollen +standardOutputDescription=Inline-signierte OpenPGP Nachricht + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-verify.properties b/sop-java-picocli/src/main/resources/msg_inline-verify.properties index dfa94d7..2e0d69f 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify.properties @@ -12,10 +12,15 @@ not-after.3=Accepts special value "-" for end of time. verifications-out=File to write details over successful verifications to 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 # 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.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties b/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties index a9a5722..9b70504 100644 --- a/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties +++ b/sop-java-picocli/src/main/resources/msg_inline-verify_de.properties @@ -12,10 +12,13 @@ not-after.3=Akzeptiert speziellen Wert '-' f verifications-out=Schreibe Status der Signaturprüfung in angegebene Ausgabe CERT[0..*]=Zertifikate (öffentlich Schlüssel) zur Signaturprüfung +standardInputDescription=Inline-signierte OpenPGP Nachricht +standardOutputDescription=Nachricht ohne Signaturen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_list-profiles.properties b/sop-java-picocli/src/main/resources/msg_list-profiles.properties index 6d5f1a8..3defe8e 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles.properties @@ -4,10 +4,13 @@ usage.header=Emit a list of profiles supported by the identified subcommand subcommand=Subcommand for which to list profiles +standardOutput=PROFILELIST +standardOutputDescription=List of profiles supported by the identified subcommand + 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.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties b/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties index ac03c0d..093aeb3 100644 --- a/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties +++ b/sop-java-picocli/src/main/resources/msg_list-profiles_de.properties @@ -4,10 +4,12 @@ usage.header=Gebe eine Liste von Profilen aus, welche vom angegebenen Unterbefehl unterstützt werden subcommand=Unterbefehl, für welchen Profile gelistet werden sollen +standardOutputDescription=Liste von Profilen, die der identifizierte Unterbefehl unterstützt + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.parameterListHeading=%nParameter:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties index 866db4b..b01f577 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -6,10 +6,16 @@ usage.description=BLABLA 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.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.descriptionHeading=%nNote:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties index 021c535..b1f008c 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs_de.properties @@ -1,19 +1,19 @@ # SPDX-FileCopyrightText: 2024 Paul Schaub # # SPDX-License-Identifier: Apache-2.0 -usage.headerHeading=OpenPGP Zertifikate zusammenführen%n%n +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 -usage.parameterList.0=STANDARDIN -usage.parameterList.1=STANDARDOUT +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=%nHinweise:%n +usage.descriptionHeading=%nHinweis:%n usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key.properties b/sop-java-picocli/src/main/resources/msg_revoke-key.properties index c7d72b3..f68b774 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key.properties @@ -7,10 +7,15 @@ no-armor=ASCII armor the output 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...). +standardInput=KEYS +standardInputDescription=OpenPGP key that shall be revoked +standardOutput=CERTS +standardOutputDescription=Revocation certificate + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 -usage.descriptionHeading=%nDescription:%n +usage.descriptionHeading=D%nescription:%n usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties index 95db272..fa8c5b4 100644 --- a/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_revoke-key_de.properties @@ -7,10 +7,13 @@ no-armor=Sch with-key-password.0=Passwort zum Entsperren der privaten Schlüssel 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 # Generic TODO: Remove when bumping picocli to 4.7.0 usage.descriptionHeading=%nBeschreibung:%n usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index d5d997a..097a2e2 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -11,6 +11,7 @@ locale=Locale for description texts usage.synopsisHeading=Usage:\u0020 usage.commandListHeading=%nCommands:%n usage.optionListHeading=%nOptions:%n +usage.parameterListHeading=%nParameters:%n usage.footerHeading=Powered by picocli%n standardInputHeading=%nInput:%n diff --git a/sop-java-picocli/src/main/resources/msg_sop_de.properties b/sop-java-picocli/src/main/resources/msg_sop_de.properties index 73efe89..99d28a7 100644 --- a/sop-java-picocli/src/main/resources/msg_sop_de.properties +++ b/sop-java-picocli/src/main/resources/msg_sop_de.properties @@ -11,6 +11,7 @@ locale=Gebietsschema f usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n usage.optionListHeading=%nOptionen:%n +usage.parameterListHeading=%nParameter:%n usage.footerHeading=Powered by Picocli%n standardInputHeading=%nEingabe:%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties index dd4446d..e12fbbc 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -10,10 +10,15 @@ with-key-password.1=Is an INDIRECT data type (e.g. file, environment variable, f 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.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_update-key_de.properties b/sop-java-picocli/src/main/resources/msg_update-key_de.properties index 86b999e..1b8a84d 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -10,9 +10,12 @@ with-key-password.1=Ist INDIREKTER Datentyp (z.B.. Datei, Umgebungsvariable, Dat 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.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index 9e1451b..c7d0168 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -6,9 +6,11 @@ extended=Print an extended version string backend=Print information about the cryptographic backend sop-spec=Print the latest revision of the SOP specification targeted by the implementation +standardOutput=version information + stacktrace=Print stacktrace # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Usage:\u0020 -usage.commandListHeading = %nCommands:%n -usage.optionListHeading = %nOptions:%n +usage.commandListHeading=%nCommands:%n +usage.optionListHeading=%nOptions:%n usage.footerHeading=Powered by picocli%n diff --git a/sop-java-picocli/src/main/resources/msg_version_de.properties b/sop-java-picocli/src/main/resources/msg_version_de.properties index 608b0c6..c317916 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -6,9 +6,11 @@ extended=Gebe erweiterte Versionsinformationen 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 +standardOutput=Versionsinformationen + stacktrace=Stacktrace ausgeben # Generic TODO: Remove when bumping picocli to 4.7.0 usage.synopsisHeading=Aufruf:\u0020 usage.commandListHeading=%nBefehle:%n -usage.optionListHeading = %nOptionen:%n +usage.optionListHeading=%nOptionen:%n usage.footerHeading=Powered by Picocli%n From 4ed326a14297e2dcae58e1ad9e13dcff70ff064d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:25 +0200 Subject: [PATCH 176/238] Implement validate-userid command --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 1 + .../cli/picocli/commands/ValidateUserIdCmd.kt | 74 ++++++++++++++++++ .../src/main/resources/msg_sop.properties | 2 + .../resources/msg_validate-userid.properties | 18 +++++ .../msg_validate-userid_de.properties | 18 +++++ sop-java/src/main/kotlin/sop/SOP.kt | 15 ++-- .../kotlin/sop/exception/SOPGPException.kt | 27 +++++-- .../kotlin/sop/operation/ValidateUserId.kt | 78 +++++++++++++++++++ 8 files changed, 216 insertions(+), 17 deletions(-) create mode 100644 sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt create mode 100644 sop-java-picocli/src/main/resources/msg_validate-userid.properties create mode 100644 sop-java-picocli/src/main/resources/msg_validate-userid_de.properties create mode 100644 sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index d234e54..dc14907 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -30,6 +30,7 @@ import sop.exception.SOPGPException UpdateKeyCmd::class, MergeCertsCmd::class, CertifyUserIdCmd::class, + ValidateUserIdCmd::class, // Messaging subcommands SignCmd::class, VerifyCmd::class, diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt new file mode 100644 index 0000000..c2de148 --- /dev/null +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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) +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 = 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) + } + } +} diff --git a/sop-java-picocli/src/main/resources/msg_sop.properties b/sop-java-picocli/src/main/resources/msg_sop.properties index 097a2e2..520533a 100644 --- a/sop-java-picocli/src/main/resources/msg_sop.properties +++ b/sop-java-picocli/src/main/resources/msg_sop.properties @@ -80,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_verifiable_signature_found=No verifiable signature found. 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 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. diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid.properties b/sop-java-picocli/src/main/resources/msg_validate-userid.properties new file mode 100644 index 0000000..5cfed2d --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# 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 + +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 diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties new file mode 100644 index 0000000..8231c6a --- /dev/null +++ b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2024 Paul Schaub +# +# 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 + +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 diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index 5435cad..c5f05e2 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -60,18 +60,15 @@ interface SOP : SOPV { /** Update a key's password. */ fun changeKeyPassword(): ChangeKeyPassword - /** - * Keep a secret key up-to-date. - */ + /** Keep a secret key up-to-date. */ fun updateKey(): UpdateKey - /** - * Merge OpenPGP certificates. - */ + /** Merge OpenPGP certificates. */ fun mergeCerts(): MergeCerts - /** - * Certify OpenPGP Certificate User-IDs. - */ + /** Certify OpenPGP Certificate User-IDs. */ fun certifyUserId(): CertifyUserId + + /** Validate a UserID in an OpenPGP certificate. */ + fun validateUserId(): ValidateUserId } diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 1f9ce6b..862e1bd 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -338,9 +338,7 @@ abstract class SOPGPException : RuntimeException { } } - /** - * The primary key of a KEYS object is too weak or revoked. - */ + /** The primary key of a KEYS object is too weak or revoked. */ class PrimaryKeyBad : SOPGPException { constructor() : super() @@ -353,13 +351,26 @@ abstract class SOPGPException : RuntimeException { } } - /** - * The CERTS object has no matching User ID. - */ + /** The CERTS object has no matching User ID. */ class CertUserIdNoMatch : SOPGPException { - constructor() : super() - constructor(errorMsg: String) : super(errorMsg) + 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 diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt new file mode 100644 index 0000000..4f4c51a --- /dev/null +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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()) +} From dd12e289263557357be7287edc9400361ee61e75 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 16:56:43 +0200 Subject: [PATCH 177/238] Checkstyle --- .../kotlin/sop/operation/CertifyUserId.kt | 24 ++++++++--------- .../main/kotlin/sop/operation/MergeCerts.kt | 9 +++---- .../main/kotlin/sop/operation/UpdateKey.kt | 27 +++++++++++++------ 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt index 92fff20..642966b 100644 --- a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -4,28 +4,26 @@ package sop.operation +import java.io.IOException +import java.io.InputStream import sop.Ready import sop.exception.SOPGPException import sop.util.UTF8Util -import java.io.IOException -import java.io.InputStream interface CertifyUserId { - @Throws(SOPGPException.UnsupportedOption::class) - fun noArmor(): CertifyUserId + @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) - fun userId(userId: String): 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)) + 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.UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) fun keys(keys: InputStream): CertifyUserId @@ -33,9 +31,11 @@ interface 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) + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun certs(certs: InputStream): Ready - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws( + SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun certs(certs: ByteArray): Ready = certs(certs.inputStream()) -} \ No newline at end of file +} diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt index f60d291..f922490 100644 --- a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -4,15 +4,14 @@ package sop.operation -import sop.Ready -import sop.exception.SOPGPException 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.UnsupportedOption::class) fun noArmor(): MergeCerts @Throws(SOPGPException.BadData::class, IOException::class) fun updates(updateCerts: InputStream): MergeCerts @@ -25,4 +24,4 @@ interface MergeCerts { @Throws(SOPGPException.BadData::class, IOException::class) fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream()) -} \ No newline at end of file +} diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 1b12f6f..6c32b22 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -4,11 +4,11 @@ package sop.operation +import java.io.IOException +import java.io.InputStream import sop.Ready import sop.exception.SOPGPException import sop.util.UTF8Util -import java.io.IOException -import java.io.InputStream interface UpdateKey { @@ -24,20 +24,31 @@ interface 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)) + 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) + @Throws( + SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: InputStream): UpdateKey - @Throws(SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @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) + @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) + @Throws( + SOPGPException.BadData::class, + IOException::class, + SOPGPException.KeyIsProtected::class, + SOPGPException.PrimaryKeyBad::class) fun key(key: ByteArray): Ready = key(key.inputStream()) -} \ No newline at end of file +} From 69fbfc09a7a77040dc0b7004e3674ec07cc6d4ed Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 17:51:04 +0200 Subject: [PATCH 178/238] Implement external variants of new subcommands --- .../main/kotlin/sop/external/ExternalSOP.kt | 20 ++++++++ .../operation/CertifyUserIdExternal.kt | 48 +++++++++++++++++++ .../external/operation/MergeCertsExternal.kt | 30 ++++++++++++ .../external/operation/UpdateKeyExternal.kt | 41 ++++++++++++++++ .../operation/ValidateUserIdExternal.kt | 38 +++++++++++++++ 5 files changed, 177 insertions(+) create mode 100644 external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt create mode 100644 external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 27c93ae..8ab7737 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -69,6 +69,14 @@ class ExternalSOP( override fun changeKeyPassword(): ChangeKeyPassword = ChangeKeyPasswordExternal(binaryName, properties) + override fun updateKey(): UpdateKey = UpdateKeyExternal(binaryName, properties) + + override fun mergeCerts(): MergeCerts = MergeCertsExternal(binaryName, properties) + + override fun certifyUserId(): CertifyUserId = CertifyUserIdExternal(binaryName, properties) + + override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties) + /** * This interface can be used to provide a directory in which external SOP binaries can * temporarily store additional results of OpenPGP operations such that the binding classes can @@ -169,6 +177,18 @@ class ExternalSOP( UnsupportedProfile.EXIT_CODE -> throw UnsupportedProfile( "External SOP backend reported error UnsupportedProfile ($exitCode):\n$errorMessage") + NoHardwareKeyFound.EXIT_CODE -> + throw NoHardwareKeyFound( + "External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage") + HardwareKeyFailure.EXIT_CODE -> + throw HardwareKeyFailure( + "External SOP backend reported error 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? else -> diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt new file mode 100644 index 0000000..abf4d50 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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 = 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) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt new file mode 100644 index 0000000..0869fab --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt new file mode 100644 index 0000000..9aa1d29 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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) +} diff --git a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt new file mode 100644 index 0000000..867a755 --- /dev/null +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Paul Schaub +// +// 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 = 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 + } +} From 0bb50952c5baa48d237990bd739102b192d280e8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:03 +0200 Subject: [PATCH 179/238] Show endOfOptions delimiter in help --- .../kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt | 7 ++----- .../kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt index 71ef79f..228809b 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/CertifyUserIdCmd.kt @@ -6,10 +6,8 @@ package sop.cli.picocli.commands import java.io.IOException import picocli.CommandLine.Command -import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Option import picocli.CommandLine.Parameters -import picocli.CommandLine.Spec import sop.cli.picocli.SopCLI import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.UnsupportedOption @@ -17,11 +15,10 @@ import sop.exception.SOPGPException.UnsupportedOption @Command( name = "certify-userid", resourceBundle = "msg_certify-userid", - exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE) + exitCodeOnInvalidInput = UnsupportedOption.EXIT_CODE, + showEndOfOptionsDelimiterInUsageHelp = true) class CertifyUserIdCmd : AbstractSopCmd() { - @Spec var spec: CommandSpec? = null - @Option(names = ["--no-armor"], negatable = true) var armor = true @Option(names = ["--userid"], required = true, arity = "1..*", paramLabel = "USERID") diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt index c2de148..da81a27 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -15,7 +15,8 @@ import sop.util.HexUtil.Companion.bytesToHex @Command( name = "validate-userid", resourceBundle = "msg_validate-userid", - exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE) + exitCodeOnInvalidInput = SOPGPException.MissingArg.EXIT_CODE, + showEndOfOptionsDelimiterInUsageHelp = true) class ValidateUserIdCmd : AbstractSopCmd() { @Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false From 40ccb8cc9936f8d32252e67fcbfe7e853c3ad9da Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:25:17 +0200 Subject: [PATCH 180/238] Add first test for new commands --- .../test/java/sop/cli/picocli/SOPTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index fe49472..4d36322 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -13,10 +13,12 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; import sop.operation.Armor; +import sop.operation.CertifyUserId; import sop.operation.ChangeKeyPassword; import sop.operation.Dearmor; import sop.operation.Decrypt; @@ -29,7 +31,10 @@ import sop.operation.InlineVerify; import sop.operation.DetachedSign; import sop.operation.DetachedVerify; import sop.operation.ListProfiles; +import sop.operation.MergeCerts; import sop.operation.RevokeKey; +import sop.operation.UpdateKey; +import sop.operation.ValidateUserId; import sop.operation.Version; public class SOPTest { @@ -52,6 +57,30 @@ public class SOPTest { @Test public void UnsupportedSubcommandsTest() { SOP nullCommandSOP = new SOP() { + @NotNull + @Override + public ValidateUserId validateUserId() { + return null; + } + + @NotNull + @Override + public CertifyUserId certifyUserId() { + return null; + } + + @NotNull + @Override + public MergeCerts mergeCerts() { + return null; + } + + @NotNull + @Override + public UpdateKey updateKey() { + return null; + } + @Override public Version version() { return null; @@ -140,6 +169,11 @@ public class SOPTest { commands.add(new String[] {"sign"}); commands.add(new String[] {"verify", "signature.asc", "cert.asc"}); commands.add(new String[] {"version"}); + commands.add(new String[] {"list-profiles", "generate-key"}); + commands.add(new String[] {"certify-userid", "--userid", "Alice ", "--", "alice.pgp"}); + commands.add(new String[] {"validate-userid", "Alice ", "bob.pgp", "--", "alice.pgp"}); + commands.add(new String[] {"update-key"}); + commands.add(new String[] {"merge-certs"}); for (String[] command : commands) { int exit = SopCLI.execute(command); From f7cc9ab816f51ffa17780bd437a960443fbaddc2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 18:40:55 +0200 Subject: [PATCH 181/238] Fix nullability of sop commands --- .../test/java/sop/cli/picocli/SOPTest.java | 5 --- sop-java/src/main/kotlin/sop/SOP.kt | 34 +++++++++---------- sop-java/src/main/kotlin/sop/SOPV.kt | 8 ++--- 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java index 4d36322..62c7581 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/SOPTest.java @@ -13,7 +13,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.SOP; import sop.exception.SOPGPException; @@ -57,25 +56,21 @@ public class SOPTest { @Test public void UnsupportedSubcommandsTest() { SOP nullCommandSOP = new SOP() { - @NotNull @Override public ValidateUserId validateUserId() { return null; } - @NotNull @Override public CertifyUserId certifyUserId() { return null; } - @NotNull @Override public MergeCerts mergeCerts() { return null; } - @NotNull @Override public UpdateKey updateKey() { return null; diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index c5f05e2..fbd0428 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -15,60 +15,60 @@ import sop.operation.* interface SOP : SOPV { /** Generate a secret key. */ - fun generateKey(): GenerateKey + fun generateKey(): GenerateKey? /** 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. */ - fun sign(): DetachedSign = detachedSign() + fun sign(): DetachedSign? = detachedSign() /** * 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 * [detachedSign] instead. */ - fun inlineSign(): InlineSign + fun inlineSign(): InlineSign? /** Detach signatures from an inline signed message. */ - fun inlineDetach(): InlineDetach + fun inlineDetach(): InlineDetach? /** Encrypt a message. */ - fun encrypt(): Encrypt + fun encrypt(): Encrypt? /** Decrypt a message. */ - fun decrypt(): Decrypt + fun decrypt(): Decrypt? /** Convert binary OpenPGP data to ASCII. */ - fun armor(): Armor + fun armor(): Armor? /** Converts ASCII armored OpenPGP data to binary. */ - fun dearmor(): Dearmor + fun dearmor(): Dearmor? /** List supported [Profiles][Profile] of a subcommand. */ - fun listProfiles(): ListProfiles + fun listProfiles(): ListProfiles? /** Revoke one or more secret keys. */ - fun revokeKey(): RevokeKey + fun revokeKey(): RevokeKey? /** Update a key's password. */ - fun changeKeyPassword(): ChangeKeyPassword + fun changeKeyPassword(): ChangeKeyPassword? /** Keep a secret key up-to-date. */ - fun updateKey(): UpdateKey + fun updateKey(): UpdateKey? /** Merge OpenPGP certificates. */ - fun mergeCerts(): MergeCerts + fun mergeCerts(): MergeCerts? /** Certify OpenPGP Certificate User-IDs. */ - fun certifyUserId(): CertifyUserId + fun certifyUserId(): CertifyUserId? /** Validate a UserID in an OpenPGP certificate. */ - fun validateUserId(): ValidateUserId + fun validateUserId(): ValidateUserId? } diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt index d331559..58a7f13 100644 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -12,23 +12,23 @@ import sop.operation.Version interface SOPV { /** Get information about the implementations name and version. */ - fun version(): Version + fun version(): Version? /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. */ - fun verify(): DetachedVerify = detachedVerify() + fun verify(): DetachedVerify? = detachedVerify() /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. */ - fun detachedVerify(): DetachedVerify + 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 + fun inlineVerify(): InlineVerify? } From f1bdce99cb4ccc14d76db6f2bd425f3766cb8388 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 19:19:56 +0200 Subject: [PATCH 182/238] Document endOfOptionsDelimiter --- .../src/main/resources/msg_certify-userid.properties | 2 ++ .../src/main/resources/msg_certify-userid_de.properties | 2 ++ .../src/main/resources/msg_validate-userid.properties | 2 ++ .../src/main/resources/msg_validate-userid_de.properties | 2 ++ 4 files changed, 8 insertions(+) diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid.properties b/sop-java-picocli/src/main/resources/msg_certify-userid.properties index 252aae4..36dc6f4 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid.properties @@ -14,6 +14,8 @@ 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 diff --git a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties index 9f0a673..d634c59 100644 --- a/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_certify-userid_de.properties @@ -12,6 +12,8 @@ KEYS[0..*]=Private Schl 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 diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid.properties b/sop-java-picocli/src/main/resources/msg_validate-userid.properties index 5cfed2d..d25fa3a 100644 --- a/sop-java-picocli/src/main/resources/msg_validate-userid.properties +++ b/sop-java-picocli/src/main/resources/msg_validate-userid.properties @@ -9,6 +9,8 @@ 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 diff --git a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties index 8231c6a..f919465 100644 --- a/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties +++ b/sop-java-picocli/src/main/resources/msg_validate-userid_de.properties @@ -9,6 +9,8 @@ CERTS[1..*]=Autorit 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 From 4cf410a9f9282fb142d922ac30efc32e66e29286 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:26:36 +0200 Subject: [PATCH 183/238] Bump version --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index b4908ee..c757e1e 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '10.1.2' + shortVersion = '11.0.0' isSnapshot = true minAndroidSdk = 10 javaSourceCompatibility = 11 From 7f1c1b1aae0e27b8f0c5efdbbb436a1ae60b4ced Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Sep 2024 20:49:00 +0200 Subject: [PATCH 184/238] Fix documentation of merge-certs command --- sop-java-picocli/src/main/resources/msg_merge-certs.properties | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/resources/msg_merge-certs.properties b/sop-java-picocli/src/main/resources/msg_merge-certs.properties index b01f577..8c0bfa3 100644 --- a/sop-java-picocli/src/main/resources/msg_merge-certs.properties +++ b/sop-java-picocli/src/main/resources/msg_merge-certs.properties @@ -2,7 +2,8 @@ # # SPDX-License-Identifier: Apache-2.0 usage.headerHeading=Merge OpenPGP certificates%n -usage.description=BLABLA +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 From 5105b6f4addc139e21b1e6025927a4b3a98fa1a0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 8 Apr 2025 13:13:15 +0200 Subject: [PATCH 185/238] Remove call to explicitly set bundle to fix native image --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index dc14907..943f0f3 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -85,8 +85,6 @@ class SopCLI { return CommandLine(SopCLI::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) // render Input/Output sections in help command From b300be42a4c2c3c1be0ee1e70dde9ceb3d1ffb72 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 8 Apr 2025 17:08:06 +0200 Subject: [PATCH 186/238] validate-userid: Add --validate-at option --- .../sop/external/operation/ValidateUserIdExternal.kt | 5 +++++ .../kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt | 7 +++++++ sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt | 3 +++ 3 files changed, 15 insertions(+) diff --git a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt index 867a755..581d6f5 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -8,6 +8,7 @@ import java.io.InputStream import java.util.* import sop.external.ExternalSOP import sop.operation.ValidateUserId +import sop.util.UTCUtil class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId { @@ -35,4 +36,8 @@ class ValidateUserIdExternal(binary: String, environment: Properties) : Validate .bytes return true } + + override fun validateAt(date: Date): ValidateUserId = apply { + commandList.add("--validate-at=${UTCUtil.formatUTCDate(date)}") + } } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt index da81a27..9a09a15 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -11,6 +11,7 @@ import picocli.CommandLine.Parameters import sop.cli.picocli.SopCLI import sop.exception.SOPGPException import sop.util.HexUtil.Companion.bytesToHex +import java.util.* @Command( name = "validate-userid", @@ -21,6 +22,8 @@ class ValidateUserIdCmd : AbstractSopCmd() { @Option(names = ["--addr-spec-only"]) var addrSpecOnly: Boolean = false + @Option(names = ["--validate-at"]) var validateAt: Date? = null + @Parameters(index = "0", arity = "1", paramLabel = "USERID") lateinit var userId: String @Parameters(index = "1..*", arity = "1..*", paramLabel = "CERTS") @@ -34,6 +37,10 @@ class ValidateUserIdCmd : AbstractSopCmd() { validateUserId.addrSpecOnly() } + if (validateAt != null) { + validateUserId.validateAt(validateAt!!) + } + validateUserId.userId(userId) for (authority in authorities) { diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt index 4f4c51a..fb8cab6 100644 --- a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -7,6 +7,7 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.exception.SOPGPException +import java.util.* /** Subcommand to validate UserIDs on certificates. */ interface ValidateUserId { @@ -75,4 +76,6 @@ interface ValidateUserId { @Throws( SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream()) + + fun validateAt(date: Date): ValidateUserId } From 082cbde869e72615eec2ca6ffe50570f545cabf4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 12:26:29 +0200 Subject: [PATCH 187/238] MergeCertsCmd: Fix default value of armor --- .../src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt index 16d56e3..3dcef38 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/MergeCertsCmd.kt @@ -16,7 +16,7 @@ import sop.exception.SOPGPException exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) class MergeCertsCmd : AbstractSopCmd() { - @CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = false + @CommandLine.Option(names = ["--no-armor"], negatable = true) var armor = true @CommandLine.Parameters(paramLabel = "CERTS") var updates: List = listOf() From a72545e3b9d4f2cb2ca8be3c36b7a7a3ced49296 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 12:46:50 +0200 Subject: [PATCH 188/238] Fix formatting --- .../src/main/kotlin/sop/cli/picocli/SopCLI.kt | 8 ++++++-- .../kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt | 2 +- sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 943f0f3..98701fc 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -88,7 +88,10 @@ class SopCLI { // Hide generate-completion command subcommands["generate-completion"]?.commandSpec?.usageMessage()?.hidden(true) // render Input/Output sections in help command - subcommands.values.filter { (it.getCommand() as Any) is AbstractSopCmd } // Only for AbstractSopCmd objects + subcommands.values + .filter { + (it.getCommand() as Any) is AbstractSopCmd + } // Only for AbstractSopCmd objects .forEach { (it.getCommand() as AbstractSopCmd).installIORenderer(it) } // overwrite executable name commandName = EXECUTABLE_NAME @@ -96,7 +99,8 @@ class SopCLI { executionExceptionHandler = SOPExecutionExceptionHandler() exitCodeExceptionMapper = SOPExceptionExitCodeMapper() isCaseInsensitiveEnumValuesAllowed = true - }.execute(*args) + } + .execute(*args) } } diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt index 9a09a15..b83e5a8 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/ValidateUserIdCmd.kt @@ -5,13 +5,13 @@ package sop.cli.picocli.commands import java.io.IOException +import java.util.* import picocli.CommandLine.Command import picocli.CommandLine.Option import picocli.CommandLine.Parameters import sop.cli.picocli.SopCLI import sop.exception.SOPGPException import sop.util.HexUtil.Companion.bytesToHex -import java.util.* @Command( name = "validate-userid", diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt index fb8cab6..fe20fd4 100644 --- a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -6,8 +6,8 @@ package sop.operation import java.io.IOException import java.io.InputStream -import sop.exception.SOPGPException import java.util.* +import sop.exception.SOPGPException /** Subcommand to validate UserIDs on certificates. */ interface ValidateUserId { From 65aa0afd4ee760b60a8b6162551a54f0b5bd113d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 12:47:00 +0200 Subject: [PATCH 189/238] Add new Exception types --- .../kotlin/sop/exception/SOPGPException.kt | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt index 862e1bd..9df1628 100644 --- a/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt +++ b/sop-java/src/main/kotlin/sop/exception/SOPGPException.kt @@ -16,6 +16,22 @@ abstract class SOPGPException : RuntimeException { abstract fun getExitCode(): Int + /** An otherwise unspecified failure occurred */ + class UnspecificFailure : SOPGPException { + + constructor(message: String) : super(message) + + constructor(message: String, e: Throwable) : super(message, e) + + constructor(e: Throwable) : super(e) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 1 + } + } + /** No acceptable signatures found (sop verify, inline-verify). */ class NoSignature : SOPGPException { @JvmOverloads @@ -378,4 +394,23 @@ abstract class SOPGPException : RuntimeException { const val EXIT_CODE = 107 } } + + /** + * Key not certification-capable (e.g., expired, revoked, unacceptable usage flags) (sop + * certify-userid) + */ + class KeyCannotCertify : SOPGPException { + + constructor(message: String) : super(message) + + constructor(message: String, e: Throwable) : super(message, e) + + constructor(e: Throwable) : super(e) + + override fun getExitCode(): Int = EXIT_CODE + + companion object { + const val EXIT_CODE = 109 + } + } } From 68bab9cbb433ba0d5f1ee91bd3f6270e9781f6ea Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 13:50:35 +0200 Subject: [PATCH 190/238] reuse: convert dep5 file to toml file --- REUSE.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/REUSE.toml b/REUSE.toml index f82d8f8..7e1b250 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -1,6 +1,6 @@ -# SPDX-FileCopyrightText: 2025 Paul Schaub +# SPDX-FileCopyrightText: 2025 Paul Schaub # -# SPDX-License-Identifier: Apache-2.0 +# SPDX-License-Identifier: CC0-1.0 version = 1 SPDX-PackageName = "SOP-Java" From a8497617d58e5c70391715713d809a74e99fed5a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 13:50:57 +0200 Subject: [PATCH 191/238] Add basic test for certify-userid and validate-userid subcommands --- .../operation/CertifyValidateUserIdTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java new file mode 100644 index 0000000..eabda44 --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.SOP; +import sop.exception.SOPGPException; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class CertifyValidateUserIdTest { + + static Stream provideInstances() { + return AbstractSOPTest.provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void certifyUserId(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .withKeyPassword("sw0rdf1sh") + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceCert = sop.extractCert() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .key(bobKey) + .getBytes(); + + // Alice has her own user-id self-certified + assertTrue(sop.validateUserId() + .authorities(aliceCert) + .userId("Alice ") + .subjects(aliceCert)); + + // Alice has not yet certified Bobs user-id + assertFalse(sop.validateUserId() + .authorities(aliceCert) + .userId("Bob ") + .subjects(bobCert)); + + byte[] bobCertifiedByAlice = sop.certifyUserId() + .userId("Bob ") + .withKeyPassword("sw0rdf1sh") + .keys(aliceKey) + .certs(bobCert) + .getBytes(); + + assertTrue(sop.validateUserId() + .userId("Bob ") + .authorities(aliceCert) + .subjects(bobCertifiedByAlice)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void addPetName(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceCert = sop.extractCert() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .key(bobKey) + .getBytes(); + + assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> + sop.certifyUserId() + .userId("Bobby") + .keys(aliceKey) + .certs(bobCert) + .getBytes()); + + byte[] bobWithPetName = sop.certifyUserId() + .userId("Bobby") + .noRequireSelfSig() + .keys(aliceKey) + .certs(bobCert) + .getBytes(); + + assertTrue(sop.validateUserId() + .userId("Bobby") + .authorities(aliceCert) + .subjects(bobWithPetName)); + } +} From a8cfb8fbf468f324d39cf00a2f501ade96f54a83 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 14:17:38 +0200 Subject: [PATCH 192/238] Improve test --- .../operation/CertifyValidateUserIdTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index eabda44..488db9a 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -49,13 +49,15 @@ public class CertifyValidateUserIdTest { assertTrue(sop.validateUserId() .authorities(aliceCert) .userId("Alice ") - .subjects(aliceCert)); + .subjects(aliceCert), + "Alice accepts her own self-certified user-id"); // Alice has not yet certified Bobs user-id assertFalse(sop.validateUserId() .authorities(aliceCert) .userId("Bob ") - .subjects(bobCert)); + .subjects(bobCert), + "Alice has not yet certified Bobs user-id"); byte[] bobCertifiedByAlice = sop.certifyUserId() .userId("Bob ") @@ -67,7 +69,8 @@ public class CertifyValidateUserIdTest { assertTrue(sop.validateUserId() .userId("Bob ") .authorities(aliceCert) - .subjects(bobCertifiedByAlice)); + .subjects(bobCertifiedByAlice), + "Alice accepts Bobs user-id after she certified it"); } @ParameterizedTest @@ -94,7 +97,8 @@ public class CertifyValidateUserIdTest { .userId("Bobby") .keys(aliceKey) .certs(bobCert) - .getBytes()); + .getBytes(), + "Alice cannot create a pet-name for Bob without the --no-require-self-sig flag"); byte[] bobWithPetName = sop.certifyUserId() .userId("Bobby") @@ -106,6 +110,13 @@ public class CertifyValidateUserIdTest { assertTrue(sop.validateUserId() .userId("Bobby") .authorities(aliceCert) - .subjects(bobWithPetName)); + .subjects(bobWithPetName), + "Alice accepts the pet-name she gave to Bob"); + + assertFalse(sop.validateUserId() + .userId("Bobby") + .authorities(bobWithPetName) + .subjects(bobWithPetName), + "Bob does not accept the pet-name Alice gave him"); } } From 8c077a9c131554671f3e56575e0432c297753beb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 14:36:28 +0200 Subject: [PATCH 193/238] SOP update-key: Rename --no-new-mechanisms option to --no-added-capabilities --- .../main/kotlin/sop/external/operation/UpdateKeyExternal.kt | 2 +- .../main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt | 6 +++--- sop-java/src/main/kotlin/sop/operation/UpdateKey.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt index 9aa1d29..99df15e 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -21,7 +21,7 @@ class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey { override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") } - override fun noNewMechanisms(): UpdateKey = apply { commandList.add("--no-new-mechanisms") } + override fun noAddedCapabilities(): UpdateKey = apply { commandList.add("--no-added-capabilities") } override fun withKeyPassword(password: ByteArray): UpdateKey = apply { commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt index 08f9297..931f241 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/UpdateKeyCmd.kt @@ -20,7 +20,7 @@ class UpdateKeyCmd : AbstractSopCmd() { @Option(names = ["--signing-only"]) var signingOnly = false - @Option(names = ["--no-new-mechanisms"]) var noNewMechanisms = false + @Option(names = ["--no-added-capabilities"]) var noAddedCapabilities = false @Option(names = ["--with-key-password"], paramLabel = "PASSWORD") var withKeyPassword: List = listOf() @@ -38,8 +38,8 @@ class UpdateKeyCmd : AbstractSopCmd() { updateKey.signingOnly() } - if (noNewMechanisms) { - updateKey.noNewMechanisms() + if (noAddedCapabilities) { + updateKey.noAddedCapabilities() } for (passwordFileName in withKeyPassword) { diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 6c32b22..f0a7c52 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -21,7 +21,7 @@ interface UpdateKey { @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey - @Throws(SOPGPException.UnsupportedOption::class) fun noNewMechanisms(): UpdateKey + @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: String): UpdateKey = From dea7e905a977154d3a563223cfbcfafd96f551eb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 14:36:49 +0200 Subject: [PATCH 194/238] Document update key --- .../main/kotlin/sop/operation/UpdateKey.kt | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index f0a7c52..9a31310 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -19,25 +19,61 @@ interface UpdateKey { */ fun noArmor(): UpdateKey + /** + * Allow key to be used for signing only. + * If this option is not present, the operation may add a new, encryption-capable component key. + */ @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey + /** + * Do not allow adding new capabilities to the key. + * If this option is not present, the operation may add support for new capabilities to the key. + */ @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey + /** + * Provide a passphrase for unlocking the secret key. + * + * @param password password + */ @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) + /** + * Provide a passphrase for unlocking the secret key. + * + * @param password password + */ @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) fun withKeyPassword(password: ByteArray): UpdateKey + /** + * Provide certificates that might contain updated signatures or third-party certifications. + * These certificates will be merged into the key. + * + * @param certs input stream of certificates + */ @Throws( SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: InputStream): UpdateKey + /** + * Provide certificates that might contain updated signatures or third-party certifications. + * These certificates will be merged into the key. + * + * @param certs binary certificates + */ @Throws( SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) + /** + * Provide the OpenPGP key to update. + * + * @param key input stream containing the key + * @return handle to acquire the updated OpenPGP key from + */ @Throws( SOPGPException.BadData::class, IOException::class, @@ -45,6 +81,12 @@ interface UpdateKey { SOPGPException.PrimaryKeyBad::class) fun key(key: InputStream): Ready + /** + * Provide the OpenPGP key to update. + * + * @param key binary OpenPGP key + * @return handle to acquire the updated OpenPGP key from + */ @Throws( SOPGPException.BadData::class, IOException::class, From 091b5f9a5e19e1605e2981f440958d7f4952ef65 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 10 Apr 2025 15:26:00 +0200 Subject: [PATCH 195/238] Add test for certifying with revoked key --- .../operation/CertifyValidateUserIdTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index 488db9a..e05923c 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -119,4 +119,35 @@ public class CertifyValidateUserIdTest { .subjects(bobWithPetName), "Bob does not accept the pet-name Alice gave him"); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void certifyWithRevokedKey(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceRevokedCert = sop.revokeKey() + .keys(aliceKey) + .getBytes(); + byte[] aliceRevokedKey = sop.updateKey() + .mergeCerts(aliceRevokedCert) + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .key(bobKey) + .getBytes(); + + assertThrows(SOPGPException.KeyCannotCertify.class, () -> + sop.certifyUserId() + .userId("Bob ") + .keys(aliceRevokedKey) + .certs(bobCert) + .getBytes()); + } } From 138e275bb6ca4241343d3bc50bec6ec55704d030 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 14 Apr 2025 10:58:58 +0200 Subject: [PATCH 196/238] Fix formatting issues --- .../kotlin/sop/external/operation/UpdateKeyExternal.kt | 4 +++- sop-java/src/main/kotlin/sop/operation/UpdateKey.kt | 8 ++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt index 99df15e..b84f452 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/UpdateKeyExternal.kt @@ -21,7 +21,9 @@ class UpdateKeyExternal(binary: String, environment: Properties) : UpdateKey { override fun signingOnly(): UpdateKey = apply { commandList.add("--signing-only") } - override fun noAddedCapabilities(): UpdateKey = apply { commandList.add("--no-added-capabilities") } + override fun noAddedCapabilities(): UpdateKey = apply { + commandList.add("--no-added-capabilities") + } override fun withKeyPassword(password: ByteArray): UpdateKey = apply { commandList.add("--with-key-password=@ENV:KEY_PASSWORD_$argCount") diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 9a31310..1226ed5 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -20,14 +20,14 @@ interface UpdateKey { fun noArmor(): UpdateKey /** - * Allow key to be used for signing only. - * If this option is not present, the operation may add a new, encryption-capable component key. + * Allow key to be used for signing only. If this option is not present, the operation may add a + * new, encryption-capable component key. */ @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey /** - * Do not allow adding new capabilities to the key. - * If this option is not present, the operation may add support for new capabilities to the key. + * Do not allow adding new capabilities to the key. If this option is not present, the operation + * may add support for new capabilities to the key. */ @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey From be460fabab162a17dde5c4ad131cc1d8f17c12ca Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 6 May 2025 12:18:05 +0200 Subject: [PATCH 197/238] Add test for certifying without ASCII armor --- .../operation/CertifyValidateUserIdTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index e05923c..c97fda7 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -73,6 +73,45 @@ public class CertifyValidateUserIdTest { "Alice accepts Bobs user-id after she certified it"); } + @ParameterizedTest + @MethodSource("provideInstances") + public void certifyUserIdUnarmored(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .noArmor() + .withKeyPassword("sw0rdf1sh") + .userId("Alice ") + .generate() + .getBytes(); + byte[] aliceCert = sop.extractCert() + .noArmor() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .noArmor() + .userId("Bob ") + .generate() + .getBytes(); + byte[] bobCert = sop.extractCert() + .noArmor() + .key(bobKey) + .getBytes(); + + byte[] bobCertifiedByAlice = sop.certifyUserId() + .noArmor() + .userId("Bob ") + .withKeyPassword("sw0rdf1sh") + .keys(aliceKey) + .certs(bobCert) + .getBytes(); + + assertTrue(sop.validateUserId() + .userId("Bob ") + .authorities(aliceCert) + .subjects(bobCertifiedByAlice), + "Alice accepts Bobs user-id after she certified it"); + } + @ParameterizedTest @MethodSource("provideInstances") public void addPetName(SOP sop) throws IOException { From 38c5a947dd5179b3b77b38bab8d0ae425e61fb9a Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 May 2025 15:16:30 +0200 Subject: [PATCH 198/238] Add MergeCertsTest --- .../testsuite/operation/MergeCertsTest.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java new file mode 100644 index 0000000..04a083e --- /dev/null +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.operation; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import sop.SOP; +import sop.util.HexUtil; + +import java.io.IOException; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +public class MergeCertsTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testMergeWithItself(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(cert) + .baseCertificates(cert) + .getBytes(); + + System.out.println("cert"); + System.out.println(new String(sop.armor().data(cert).getBytes())); + System.out.println("merged"); + System.out.println(new String(sop.armor().data(merged).getBytes())); + assertArrayEquals(cert, merged); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testApplyBaseToUpdate(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] update = sop.revokeKey() + .noArmor() + .keys(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(cert) + .baseCertificates(update) + .getBytes(); + + assertArrayEquals(update, merged); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testApplyUpdateToBase(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] update = sop.revokeKey() + .noArmor() + .keys(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(update) + .baseCertificates(cert) + .getBytes(); + + assertArrayEquals(update, merged); + } +} From 9360b0e8ce321edc6d274c6473b1b79622033f47 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 May 2025 15:36:36 +0200 Subject: [PATCH 199/238] Remove println statements --- .../src/main/java/sop/testsuite/operation/MergeCertsTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 04a083e..09387e9 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -41,10 +41,6 @@ public class MergeCertsTest extends AbstractSOPTest { .baseCertificates(cert) .getBytes(); - System.out.println("cert"); - System.out.println(new String(sop.armor().data(cert).getBytes())); - System.out.println("merged"); - System.out.println(new String(sop.armor().data(merged).getBytes())); assertArrayEquals(cert, merged); } From 6d23d3771d310e538e7a49b1ba92a46279c619c9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 13 May 2025 15:37:48 +0200 Subject: [PATCH 200/238] Remove unused import --- .../src/main/java/sop/testsuite/operation/MergeCertsTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 09387e9..3d40a99 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -8,7 +8,6 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import sop.SOP; -import sop.util.HexUtil; import java.io.IOException; import java.util.stream.Stream; From 77106942d1d4059b5b772aebbafa322604aa5f16 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 May 2025 00:36:16 +0200 Subject: [PATCH 201/238] Add tests for MergeCerts command --- .../testsuite/operation/MergeCertsTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 3d40a99..7bc99d1 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -4,6 +4,7 @@ package sop.testsuite.operation; +import kotlin.collections.ArraysKt; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; @@ -43,6 +44,52 @@ public class MergeCertsTest extends AbstractSOPTest { assertArrayEquals(cert, merged); } + @ParameterizedTest + @MethodSource("provideInstances") + public void testMergeWithItselfArmored(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .key(key) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .updates(cert) + .baseCertificates(cert) + .getBytes(); + + assertArrayEquals(cert, merged); + } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testMergeWithItselfViaBase(SOP sop) throws IOException { + byte[] key = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] cert = sop.extractCert() + .noArmor() + .key(key) + .getBytes(); + + byte[] certs = ArraysKt.plus(cert, cert); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(cert) + .baseCertificates(certs) + .getBytes(); + + assertArrayEquals(cert, merged); + } + @ParameterizedTest @MethodSource("provideInstances") public void testApplyBaseToUpdate(SOP sop) throws IOException { @@ -98,4 +145,39 @@ public class MergeCertsTest extends AbstractSOPTest { assertArrayEquals(update, merged); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException { + byte[] aliceKey = sop.generateKey() + .noArmor() + .userId("Alice ") + .generate() + .getBytes(); + + byte[] aliceCert = sop.extractCert() + .noArmor() + .key(aliceKey) + .getBytes(); + + byte[] bobKey = sop.generateKey() + .noArmor() + .userId("Bob ") + .generate() + .getBytes(); + + byte[] bobCert = sop.extractCert() + .noArmor() + .key(bobKey) + .getBytes(); + + byte[] merged = sop.mergeCerts() + .noArmor() + .updates(bobCert) + .baseCertificates(aliceCert) + .getBytes(); + + assertArrayEquals(aliceCert, merged); + } + } From 4ef5444e782cbfba98014311ed97425b83f29764 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 15 May 2025 01:09:36 +0200 Subject: [PATCH 202/238] Test key generation with supported profiles --- .../testsuite/operation/GenerateKeyTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java index 4a5da58..b63b4b8 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.condition.EnabledIf; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import sop.Profile; import sop.SOP; import sop.exception.SOPGPException; import sop.testsuite.JUtils; @@ -16,9 +17,11 @@ import sop.testsuite.TestData; import java.io.IOException; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") public class GenerateKeyTest extends AbstractSOPTest { @@ -118,4 +121,30 @@ public class GenerateKeyTest extends AbstractSOPTest { sop.encrypt().withCert(signingOnlyCert) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void generateKeyWithSupportedProfiles(SOP sop) throws IOException { + List profiles = sop.listProfiles() + .generateKey(); + + for (Profile profile : profiles) { + generateKeyWithProfile(sop, profile.getName()); + } + } + + private void generateKeyWithProfile(SOP sop, String profile) throws IOException { + byte[] key; + try { + key = sop.generateKey() + .profile(profile) + .userId("Alice ") + .generate() + .getBytes(); + } catch (SOPGPException.UnsupportedProfile e) { + key = null; + } + assumeTrue(key != null, "'generate-key' does not support profile '" + profile + "'."); + JUtils.assertArrayStartsWith(key, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); + } } From e5cb58468b22cd8945fd8151d74bb4cf00cb922e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 May 2025 18:11:54 +0200 Subject: [PATCH 203/238] Add aliases to Profile --- sop-java/src/main/kotlin/sop/Profile.kt | 44 ++++++++++++++++----- sop-java/src/test/java/sop/ProfileTest.java | 21 ++++++++++ 2 files changed, 56 insertions(+), 9 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt index 2125c57..2208592 100644 --- a/sop-java/src/main/kotlin/sop/Profile.kt +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -20,11 +20,15 @@ import sop.util.UTF8Util * in the IETF namespace that begins with the string `draft-` should have semantics that hew as * closely as possible to the referenced Internet Draft. * @param description a free-form description of the profile. - * @see - * SOP Spec - Profile + * @param aliases list of optional profile alias names + * @see + * [SOP Spec - Profile](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-05.html#name-profile) */ -data class Profile(val name: String, val description: Optional) { +data class Profile( + val name: String, + val description: Optional, + val aliases: List = listOf() +) { @JvmOverloads constructor( @@ -50,8 +54,18 @@ data class Profile(val name: String, val description: Optional) { * * @return string */ - override fun toString(): String = - if (description.isEmpty) name else "$name: ${description.get()}" + override fun toString(): String = buildString { + append(name) + if (!description.isEmpty || !aliases.isEmpty()) { + append(":") + } + if (!description.isEmpty) { + append(" ${description.get()}") + } + if (!aliases.isEmpty()) { + append(" (aliases: ${aliases.joinToString(separator = ", ")})") + } + } companion object { @@ -64,9 +78,21 @@ data class Profile(val name: String, val description: Optional) { @JvmStatic fun parse(string: String): Profile { return if (string.contains(": ")) { - Profile( - string.substring(0, string.indexOf(": ")), - string.substring(string.indexOf(": ") + 2).trim()) + val name = string.substring(0, string.indexOf(": ")) + var description = string.substring(string.indexOf(": ") + 2).trim() + if (description.contains("(aliases: ")) { + val aliases = + description.substring( + description.indexOf("(aliases: ") + 10, description.indexOf(")")) + description = description.substring(0, description.indexOf("(aliases: ")).trim() + Profile(name, Optional.of(description), aliases.split(", ").toList()) + } else { + if (description.isNotBlank()) { + Profile(name, Optional.of(description)) + } else { + Profile(name) + } + } } else if (string.endsWith(":")) { Profile(string.substring(0, string.length - 1)) } else { diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index 564a6af..770b88b 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -5,6 +5,9 @@ package sop; import org.junit.jupiter.api.Test; +import sop.util.Optional; + +import java.util.Arrays; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -19,6 +22,24 @@ public class ProfileTest { assertEquals("default: Use the implementers recommendations.", profile.toString()); } + @Test + public void withAliasesToString() { + Profile profile = new Profile( + "Foo", + Optional.of("Something something"), + Arrays.asList("Bar", "Baz")); + assertEquals("Foo: Something something (aliases: Bar, Baz)", profile.toString()); + } + + + @Test + public void parseWithAliases() { + Profile profile = Profile.parse("Foo: Something something (aliases: Bar, Baz)"); + assertEquals("Foo", profile.getName()); + assertEquals("Something something", profile.getDescription().get()); + assertEquals(Arrays.asList("Bar", "Baz"), profile.getAliases()); + } + @Test public void toStringNameOnly() { Profile profile = new Profile("default"); From 9677f1fd0b3a12332c08c3f03a3458f5ea5e14be Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 27 May 2025 18:34:04 +0200 Subject: [PATCH 204/238] Fix profile constructors --- sop-java/src/main/kotlin/sop/Profile.kt | 9 +++++---- sop-java/src/test/java/sop/ProfileTest.java | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt index 2208592..725bbe5 100644 --- a/sop-java/src/main/kotlin/sop/Profile.kt +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -33,8 +33,9 @@ data class Profile( @JvmOverloads constructor( name: String, - description: String? = null - ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null })) + description: String? = null, + aliases: List = listOf() + ) : this(name, Optional.ofNullable(description?.trim()?.ifBlank { null }), aliases) init { require(name.trim().isNotBlank()) { "Name cannot be empty." } @@ -85,10 +86,10 @@ data class Profile( description.substring( description.indexOf("(aliases: ") + 10, description.indexOf(")")) description = description.substring(0, description.indexOf("(aliases: ")).trim() - Profile(name, Optional.of(description), aliases.split(", ").toList()) + Profile(name, description, aliases.split(", ").toList()) } else { if (description.isNotBlank()) { - Profile(name, Optional.of(description)) + Profile(name, description) } else { Profile(name) } diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index 770b88b..41b4a2f 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -31,7 +31,6 @@ public class ProfileTest { assertEquals("Foo: Something something (aliases: Bar, Baz)", profile.toString()); } - @Test public void parseWithAliases() { Profile profile = Profile.parse("Foo: Something something (aliases: Bar, Baz)"); From e4817174217697bcaf33d227c76f36600d13be6c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 30 May 2025 12:48:24 +0200 Subject: [PATCH 205/238] Add Profile.withAliases() utility function --- sop-java/src/main/kotlin/sop/Profile.kt | 10 ++++++++++ sop-java/src/test/java/sop/ProfileTest.java | 14 ++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/Profile.kt b/sop-java/src/main/kotlin/sop/Profile.kt index 725bbe5..fd58c63 100644 --- a/sop-java/src/main/kotlin/sop/Profile.kt +++ b/sop-java/src/main/kotlin/sop/Profile.kt @@ -50,6 +50,16 @@ data class Profile( fun hasDescription() = description.isPresent + /** + * Return a copy of this [Profile] with the aliases set to the given strings. + * + * @param alias one or more alias names + * @return profile with aliases + */ + fun withAliases(vararg alias: String): Profile { + return Profile(name, description, alias.toList()) + } + /** * Convert the profile into a String for displaying. * diff --git a/sop-java/src/test/java/sop/ProfileTest.java b/sop-java/src/test/java/sop/ProfileTest.java index 41b4a2f..f418672 100644 --- a/sop-java/src/test/java/sop/ProfileTest.java +++ b/sop-java/src/test/java/sop/ProfileTest.java @@ -39,6 +39,20 @@ public class ProfileTest { assertEquals(Arrays.asList("Bar", "Baz"), profile.getAliases()); } + @Test + public void changeAliasesWithWithAliases() { + Profile p = new Profile("Foo", "Bar any Baz", Arrays.asList("tinitus", "particle")); + p = p.withAliases("fnord", "qbit"); + + assertEquals("Foo", p.getName()); + assertEquals("Bar any Baz", p.getDescription().get()); + + assertTrue(p.getAliases().contains("fnord")); + assertTrue(p.getAliases().contains("qbit")); + assertFalse(p.getAliases().contains("tinitus")); + assertFalse(p.getAliases().contains("particle")); + } + @Test public void toStringNameOnly() { Profile profile = new Profile("default"); From c5d9e57f69073fb5c320d7aff3c6f68c3d99b1fa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 30 May 2025 15:05:44 +0200 Subject: [PATCH 206/238] Add test for encrypt-decrypt using all available generate-key profiles --- .../operation/EncryptDecryptTest.java | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java index df824ca..330ca90 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -11,12 +11,15 @@ import org.junit.jupiter.params.provider.MethodSource; import sop.ByteArrayAndResult; import sop.DecryptionResult; import sop.EncryptionResult; +import sop.Profile; import sop.SOP; import sop.SessionKey; import sop.Verification; import sop.enums.EncryptAs; import sop.enums.SignatureMode; import sop.exception.SOPGPException; +import sop.operation.Decrypt; +import sop.operation.Encrypt; import sop.testsuite.TestData; import sop.testsuite.assertions.VerificationListAssert; import sop.util.Optional; @@ -25,6 +28,7 @@ import sop.util.UTCUtil; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.ParseException; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.stream.Stream; @@ -338,4 +342,55 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes()); } + + @ParameterizedTest + @MethodSource("provideInstances") + public void encryptDecryptWithAllSupportedKeyGenerationProfiles(SOP sop) throws IOException { + List profiles = sop.listProfiles().generateKey(); + + List keys = new ArrayList<>(); + List certs = new ArrayList<>(); + for (Profile p : profiles) { + byte[] k = sop.generateKey() + .profile(p) + .userId(p.getName()) + .generate() + .getBytes(); + keys.add(k); + + byte[] c = sop.extractCert() + .key(k) + .getBytes(); + certs.add(c); + } + + byte[] plaintext = "Hello, World!\n".getBytes(); + + Encrypt encrypt = sop.encrypt(); + for (byte[] c : certs) { + encrypt.withCert(c); + } + for (byte[] k : keys) { + encrypt.signWith(k); + } + + ByteArrayAndResult encRes = encrypt.plaintext(plaintext) + .toByteArrayAndResult(); + EncryptionResult eResult = encRes.getResult(); + byte[] ciphertext = encRes.getBytes(); + + for (byte[] k : keys) { + Decrypt decrypt = sop.decrypt() + .withKey(k); + for (byte[] c : certs) { + decrypt.verifyWithCert(c); + } + ByteArrayAndResult decRes = decrypt.ciphertext(ciphertext) + .toByteArrayAndResult(); + DecryptionResult dResult = decRes.getResult(); + byte[] decPlaintext = decRes.getBytes(); + assertArrayEquals(plaintext, decPlaintext); + assertEquals(certs.size(), dResult.getVerifications().size()); + } + } } From 00a02686c8e44e748faf96ca7d58abbb0c153f6c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:21:07 +0200 Subject: [PATCH 207/238] External-SOP: Fix command names --- .../main/kotlin/sop/external/operation/CertifyUserIdExternal.kt | 2 +- .../main/kotlin/sop/external/operation/MergeCertsExternal.kt | 2 +- .../kotlin/sop/external/operation/ValidateUserIdExternal.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt index abf4d50..c6c08cd 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -12,7 +12,7 @@ import sop.operation.CertifyUserId class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUserId { - private val commandList = mutableListOf(binary, "version") + private val commandList = mutableListOf(binary, "certify-userid") private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() private var argCount = 0 diff --git a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt index 0869fab..b739eb3 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/MergeCertsExternal.kt @@ -12,7 +12,7 @@ import sop.operation.MergeCerts class MergeCertsExternal(binary: String, environment: Properties) : MergeCerts { - private val commandList = mutableListOf(binary, "version") + private val commandList = mutableListOf(binary, "merge-certs") private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() private var argCount = 0 diff --git a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt index 581d6f5..cf4742b 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/ValidateUserIdExternal.kt @@ -12,7 +12,7 @@ import sop.util.UTCUtil class ValidateUserIdExternal(binary: String, environment: Properties) : ValidateUserId { - private val commandList = mutableListOf(binary, "version") + private val commandList = mutableListOf(binary, "validate-userid") private val envList = ExternalSOP.propertiesToEnv(environment).toMutableList() private var argCount = 0 From 0df80470c682fe939727836463892ef98606891f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:21:31 +0200 Subject: [PATCH 208/238] External-SOP: Extend test suite with new test classes --- .../ExternalCertifyValidateUserIdTest.java | 13 +++++++++++++ .../operation/ExternalChangeKeyPasswordTest.java | 13 +++++++++++++ .../external/operation/ExternalMergeCertsTest.java | 13 +++++++++++++ 3 files changed, 39 insertions(+) create mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java create mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java create mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java new file mode 100644 index 0000000..bb319ca --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import sop.testsuite.operation.CertifyValidateUserIdTest; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ExternalCertifyValidateUserIdTest extends CertifyValidateUserIdTest { + +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java new file mode 100644 index 0000000..42a9693 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import sop.testsuite.operation.ChangeKeyPasswordTest; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ExternalChangeKeyPasswordTest extends ChangeKeyPasswordTest { + +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java new file mode 100644 index 0000000..8b22b37 --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import org.junit.jupiter.api.condition.EnabledIf; +import sop.testsuite.operation.MergeCertsTest; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class ExternalMergeCertsTest extends MergeCertsTest { + +} From 47a6db8702d382ca2411393e456879deba27162e Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:53:24 +0200 Subject: [PATCH 209/238] External-SOP: Fix error message typo --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 8ab7737..1291433 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -182,7 +182,7 @@ class ExternalSOP( "External SOP backend reported error NoHardwareKeyFound ($exitCode):\n$errorMessage") HardwareKeyFailure.EXIT_CODE -> throw HardwareKeyFailure( - "External SOP backend reported error HardwareKeyFalure ($exitCode):\n$errorMessage") + "External SOP backend reported error HardwareKeyFailure ($exitCode):\n$errorMessage") PrimaryKeyBad.EXIT_CODE -> throw PrimaryKeyBad( "External SOP backend reported error PrimaryKeyBad ($exitCode):\n$errorMessage") From 589884672a37a3ba7b444bd1d539b73fb55bd6dd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:53:46 +0200 Subject: [PATCH 210/238] External-SOP: Properly map KeyCannotCertify error code --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index 1291433..b43b786 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -189,6 +189,9 @@ class ExternalSOP( CertUserIdNoMatch.EXIT_CODE -> throw CertUserIdNoMatch( "External SOP backend reported error CertUserIdNoMatch ($exitCode):\n$errorMessage") + KeyCannotCertify.EXIT_CODE -> + throw KeyCannotCertify( + "External SOP backend reported error KeyCannotCertify ($exitCode):\n$errorMessage") // Did you forget to add a case for a new exception type? else -> From 61206dde5310a1cec33a2f8698885f32ccc46b97 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:54:41 +0200 Subject: [PATCH 211/238] GenerateKeyTest: Provoke exception for CertCannotEncrypt test case --- .../main/java/sop/testsuite/operation/GenerateKeyTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java index b63b4b8..0d8b78e 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java @@ -119,7 +119,9 @@ public class GenerateKeyTest extends AbstractSOPTest { assertThrows(SOPGPException.CertCannotEncrypt.class, () -> sop.encrypt().withCert(signingOnlyCert) - .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8))); + .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) + .toByteArrayAndResult() + .getBytes()); } @ParameterizedTest From e1d048225b04c272632e076224325367c769d74d Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 12:57:11 +0200 Subject: [PATCH 212/238] CertifyValidateUserIdTest: unbound User-IDs do throw exceptions --- .../operation/CertifyValidateUserIdTest.java | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index c97fda7..7f9f088 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -14,7 +14,6 @@ import sop.exception.SOPGPException; import java.io.IOException; import java.util.stream.Stream; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -47,16 +46,17 @@ public class CertifyValidateUserIdTest { // Alice has her own user-id self-certified assertTrue(sop.validateUserId() - .authorities(aliceCert) - .userId("Alice ") - .subjects(aliceCert), + .authorities(aliceCert) + .userId("Alice ") + .subjects(aliceCert), "Alice accepts her own self-certified user-id"); // Alice has not yet certified Bobs user-id - assertFalse(sop.validateUserId() - .authorities(aliceCert) - .userId("Bob ") - .subjects(bobCert), + assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> + sop.validateUserId() + .authorities(aliceCert) + .userId("Bob ") + .subjects(bobCert), "Alice has not yet certified Bobs user-id"); byte[] bobCertifiedByAlice = sop.certifyUserId() @@ -67,10 +67,10 @@ public class CertifyValidateUserIdTest { .getBytes(); assertTrue(sop.validateUserId() - .userId("Bob ") - .authorities(aliceCert) - .subjects(bobCertifiedByAlice), - "Alice accepts Bobs user-id after she certified it"); + .userId("Bob ") + .authorities(aliceCert) + .subjects(bobCertifiedByAlice), + "Alice accepts Bobs user-id after she certified it"); } @ParameterizedTest @@ -132,11 +132,11 @@ public class CertifyValidateUserIdTest { .getBytes(); assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.certifyUserId() - .userId("Bobby") - .keys(aliceKey) - .certs(bobCert) - .getBytes(), + sop.certifyUserId() + .userId("Bobby") + .keys(aliceKey) + .certs(bobCert) + .getBytes(), "Alice cannot create a pet-name for Bob without the --no-require-self-sig flag"); byte[] bobWithPetName = sop.certifyUserId() @@ -147,15 +147,16 @@ public class CertifyValidateUserIdTest { .getBytes(); assertTrue(sop.validateUserId() - .userId("Bobby") - .authorities(aliceCert) - .subjects(bobWithPetName), + .userId("Bobby") + .authorities(aliceCert) + .subjects(bobWithPetName), "Alice accepts the pet-name she gave to Bob"); - assertFalse(sop.validateUserId() - .userId("Bobby") - .authorities(bobWithPetName) - .subjects(bobWithPetName), + assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> + sop.validateUserId() + .userId("Bobby") + .authorities(bobWithPetName) + .subjects(bobWithPetName), "Bob does not accept the pet-name Alice gave him"); } From ab13cc1de16a44daa2fde0ad6dde33ac8351d5d7 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 13:39:31 +0200 Subject: [PATCH 213/238] CertifyUserIdExternal: add separator before passing keys --- .../main/kotlin/sop/external/operation/CertifyUserIdExternal.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt index c6c08cd..e3661db 100644 --- a/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt +++ b/external-sop/src/main/kotlin/sop/external/operation/CertifyUserIdExternal.kt @@ -44,5 +44,5 @@ class CertifyUserIdExternal(binary: String, environment: Properties) : CertifyUs override fun certs(certs: InputStream): Ready = ExternalSOP.executeTransformingOperation( - Runtime.getRuntime(), commandList.plus(keys), envList, certs) + Runtime.getRuntime(), commandList.plus("--").plus(keys), envList, certs) } From 28d06c330d201815b2662f2b6238f647337e4a97 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 13:47:20 +0200 Subject: [PATCH 214/238] ExternalSOP: Map UnspecificError --- external-sop/src/main/kotlin/sop/external/ExternalSOP.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt index b43b786..48a5af9 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOP.kt @@ -120,6 +120,9 @@ class ExternalSOP( val errorMessage = readString(errIn) when (exitCode) { + UnspecificFailure.EXIT_CODE -> + throw UnspecificFailure( + "External SOP backend reported an unspecific error ($exitCode):\n$errorMessage") NoSignature.EXIT_CODE -> throw NoSignature( "External SOP backend reported error NoSignature ($exitCode):\n$errorMessage") From 79aece6f04059983139615afde3edaf336703760 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 14:13:36 +0200 Subject: [PATCH 215/238] SOP, SOPV: Add --debug option --- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt | 2 +- sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt index 98701fc..07caa03 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopCLI.kt @@ -64,7 +64,7 @@ class SopCLI { @JvmField var EXECUTABLE_NAME = "sop" @JvmField - @Option(names = ["--stacktrace"], scope = ScopeType.INHERIT) + @Option(names = ["--stacktrace", "--debug"], scope = ScopeType.INHERIT) var stacktrace = false @JvmStatic diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt index 9a8b4b4..fa9683f 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -45,7 +45,7 @@ class SopVCLI { @JvmField var EXECUTABLE_NAME = "sopv" @JvmField - @CommandLine.Option(names = ["--stacktrace"], scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option(names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) var stacktrace = false @JvmStatic From 5a7a8ae901f58f0bdc6feacc70115d370834bc7f Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Mon, 2 Jun 2025 14:14:22 +0200 Subject: [PATCH 216/238] MergeCertsTest: do not pass unarmored data This is done to fix external-sop tests, which rely on environment variables, which do not play nicely with binary data --- .../testsuite/operation/MergeCertsTest.java | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index 7bc99d1..b577017 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -25,18 +25,15 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testMergeWithItself(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(cert) .baseCertificates(cert) .getBytes(); @@ -69,20 +66,17 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testMergeWithItselfViaBase(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] certs = ArraysKt.plus(cert, cert); byte[] merged = sop.mergeCerts() - .noArmor() .updates(cert) .baseCertificates(certs) .getBytes(); @@ -94,23 +88,19 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testApplyBaseToUpdate(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] update = sop.revokeKey() - .noArmor() .keys(key) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(cert) .baseCertificates(update) .getBytes(); @@ -122,23 +112,19 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testApplyUpdateToBase(SOP sop) throws IOException { byte[] key = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] cert = sop.extractCert() - .noArmor() .key(key) .getBytes(); byte[] update = sop.revokeKey() - .noArmor() .keys(key) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(update) .baseCertificates(cert) .getBytes(); @@ -150,29 +136,24 @@ public class MergeCertsTest extends AbstractSOPTest { @MethodSource("provideInstances") public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException { byte[] aliceKey = sop.generateKey() - .noArmor() .userId("Alice ") .generate() .getBytes(); byte[] aliceCert = sop.extractCert() - .noArmor() .key(aliceKey) .getBytes(); byte[] bobKey = sop.generateKey() - .noArmor() .userId("Bob ") .generate() .getBytes(); byte[] bobCert = sop.extractCert() - .noArmor() .key(bobKey) .getBytes(); byte[] merged = sop.mergeCerts() - .noArmor() .updates(bobCert) .baseCertificates(aliceCert) .getBytes(); From bff4423f93ce0897f52598cc8e6805bd6332301c Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 14:30:54 +0200 Subject: [PATCH 217/238] Verification: Rename description to jsonOrDescription --- .../sop/testsuite/assertions/VerificationAssert.java | 4 ++-- sop-java/src/main/kotlin/sop/Verification.kt | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java index 63fd237..5267148 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java @@ -45,12 +45,12 @@ public final class VerificationAssert { } public VerificationAssert hasDescription(String description) { - assertEquals(description, verification.getDescription().get()); + assertEquals(description, verification.getJsonOrDescription().get()); return this; } public VerificationAssert hasDescriptionOrNull(String description) { - if (verification.getDescription().isEmpty()) { + if (verification.getJsonOrDescription().isEmpty()) { return this; } diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt index 20401a9..65a858e 100644 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -15,7 +15,7 @@ data class Verification( val signingKeyFingerprint: String, val signingCertFingerprint: String, val signatureMode: Optional, - val description: Optional + val jsonOrDescription: Optional ) { @JvmOverloads constructor( @@ -31,10 +31,15 @@ data class Verification( Optional.ofNullable(signatureMode), Optional.ofNullable(description?.trim())) + @Deprecated("Replaced by jsonOrDescription", + replaceWith = ReplaceWith("jsonOrDescription") + ) + val description = jsonOrDescription + override fun toString(): String = "${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" + (if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") + - (if (description.isPresent) " ${description.get()}" else "") + (if (jsonOrDescription.isPresent) " ${jsonOrDescription.get()}" else "") companion object { @JvmStatic From ebfde3542228c9e94b6aae20e99dda30fbe2c4d3 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 14:31:07 +0200 Subject: [PATCH 218/238] Add support for JSON POJOs --- sop-java/src/main/kotlin/sop/Verification.kt | 78 ++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt index 65a858e..a8db800 100644 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -17,6 +17,7 @@ data class Verification( val signatureMode: Optional, val jsonOrDescription: Optional ) { + @JvmOverloads constructor( creationTime: Date, @@ -31,11 +32,44 @@ data class Verification( Optional.ofNullable(signatureMode), Optional.ofNullable(description?.trim())) + @JvmOverloads + constructor( + creationTime: Date, + signingKeyFingerprint: String, + signingCertFingerprint: String, + signatureMode: SignatureMode? = null, + json: JSON, + jsonSerializer: JSONSerializer + ) : this( + creationTime, + signingKeyFingerprint, + signingCertFingerprint, + Optional.ofNullable(signatureMode), + Optional.of(jsonSerializer.serialize(json))) + @Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("jsonOrDescription") ) val description = jsonOrDescription + /** + * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the result. + * This method returns `null` if parsing fails. + * + * @param parser [JSONParser] implementation + * @return successfully parsed [JSON] POJO or `null`. + */ + fun getJson(parser: JSONParser): JSON? { + return jsonOrDescription.get() + ?.let { + try { + parser.parse(it) + } catch (e: ParseException) { + null + } + } + } + override fun toString(): String = "${UTCUtil.formatUTCDate(creationTime)} $signingKeyFingerprint $signingCertFingerprint" + (if (signatureMode.isPresent) " mode:${signatureMode.get()}" else "") + @@ -78,4 +112,48 @@ data class Verification( } } } + + /** + * POJO data class representing JSON metadata. + * + * @param signers list of supplied CERTS objects that could have issued the signature, identified by + * the name given on the command line. + * @param comment a freeform UTF-8 encoded text describing the verification + * @param ext an extension object containing arbitrary, implementation-specific data + */ + data class JSON( + val signers: List, + val comment: String?, + val ext: Any?) + + /** + * Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. + */ + fun interface JSONParser { + /** + * Parse a [JSON] POJO from the given single-line [string]. + * If the string does not represent a JSON object matching the [JSON] definition, + * this method throws a [ParseException]. + * + * @param string [String] representation of the [JSON] object. + * @return parsed [JSON] POJO + * @throws ParseException if the [string] is not a JSON string representing the [JSON] object. + */ + @Throws(ParseException::class) + fun parse(string: String): JSON + } + + /** + * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON strings. + */ + fun interface JSONSerializer { + + /** + * Serialize the given [JSON] object into a single-line JSON string. + * + * @param json JSON POJO + * @return JSON string + */ + fun serialize(json: JSON): String + } } From cdcbae7e5f4ecd448a3cabfbd3367f99d6f694f2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 23:14:23 +0200 Subject: [PATCH 219/238] Add test for JSON data parsing and serializing using a dummy implementation --- .../main/kotlin/sop/cli/picocli/SopVCLI.kt | 3 +- sop-java/src/main/kotlin/sop/Verification.kt | 68 +++++--- .../test/java/sop/VerificationJSONTest.java | 163 ++++++++++++++++++ .../src/test/java/sop/VerificationTest.java | 3 + 4 files changed, 208 insertions(+), 29 deletions(-) create mode 100644 sop-java/src/test/java/sop/VerificationJSONTest.java diff --git a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt index fa9683f..311a446 100644 --- a/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt +++ b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/SopVCLI.kt @@ -45,7 +45,8 @@ class SopVCLI { @JvmField var EXECUTABLE_NAME = "sopv" @JvmField - @CommandLine.Option(names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) + @CommandLine.Option( + names = ["--stacktrace", "--debug"], scope = CommandLine.ScopeType.INHERIT) var stacktrace = false @JvmStatic diff --git a/sop-java/src/main/kotlin/sop/Verification.kt b/sop-java/src/main/kotlin/sop/Verification.kt index a8db800..982e691 100644 --- a/sop-java/src/main/kotlin/sop/Verification.kt +++ b/sop-java/src/main/kotlin/sop/Verification.kt @@ -10,6 +10,15 @@ import sop.enums.SignatureMode import sop.util.Optional import sop.util.UTCUtil +/** + * Metadata about a verified signature. + * + * @param creationTime creation time of the signature + * @param signingKeyFingerprint fingerprint of the (sub-)key that issued the signature + * @param signingCertFingerprint fingerprint of the certificate that contains the signing key + * @param signatureMode optional signature mode (text/binary) + * @param jsonOrDescription arbitrary text or JSON data + */ data class Verification( val creationTime: Date, val signingKeyFingerprint: String, @@ -47,27 +56,28 @@ data class Verification( Optional.ofNullable(signatureMode), Optional.of(jsonSerializer.serialize(json))) - @Deprecated("Replaced by jsonOrDescription", - replaceWith = ReplaceWith("jsonOrDescription") - ) + @Deprecated("Replaced by jsonOrDescription", replaceWith = ReplaceWith("jsonOrDescription")) val description = jsonOrDescription + /** This value is `true` if the [Verification] contains extension JSON. */ + val containsJson: Boolean = + jsonOrDescription.get()?.trim()?.let { it.startsWith("{") && it.endsWith("}") } ?: false + /** - * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the result. - * This method returns `null` if parsing fails. + * Attempt to parse the [jsonOrDescription] field using the provided [JSONParser] and return the + * result. This method returns `null` if parsing fails. * * @param parser [JSONParser] implementation * @return successfully parsed [JSON] POJO or `null`. */ fun getJson(parser: JSONParser): JSON? { - return jsonOrDescription.get() - ?.let { - try { - parser.parse(it) - } catch (e: ParseException) { - null - } + return jsonOrDescription.get()?.let { + try { + parser.parse(it) + } catch (e: ParseException) { + null } + } } override fun toString(): String = @@ -116,35 +126,37 @@ data class Verification( /** * POJO data class representing JSON metadata. * - * @param signers list of supplied CERTS objects that could have issued the signature, identified by - * the name given on the command line. + * @param signers list of supplied CERTS objects that could have issued the signature, + * identified by the name given on the command line. * @param comment a freeform UTF-8 encoded text describing the verification * @param ext an extension object containing arbitrary, implementation-specific data */ - data class JSON( - val signers: List, - val comment: String?, - val ext: Any?) + data class JSON(val signers: List, val comment: String?, val ext: Any?) { - /** - * Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. - */ + /** Create a JSON object with only a list of signers. */ + constructor(signers: List) : this(signers, null, null) + + /** Create a JSON object with only a single signer. */ + constructor(signer: String) : this(listOf(signer)) + } + + /** Interface abstracting a JSON parser that parses [JSON] POJOs from single-line strings. */ fun interface JSONParser { /** - * Parse a [JSON] POJO from the given single-line [string]. - * If the string does not represent a JSON object matching the [JSON] definition, - * this method throws a [ParseException]. + * Parse a [JSON] POJO from the given single-line [string]. If the string does not represent + * a JSON object matching the [JSON] definition, this method throws a [ParseException]. * * @param string [String] representation of the [JSON] object. * @return parsed [JSON] POJO - * @throws ParseException if the [string] is not a JSON string representing the [JSON] object. + * @throws ParseException if the [string] is not a JSON string representing the [JSON] + * object. */ - @Throws(ParseException::class) - fun parse(string: String): JSON + @Throws(ParseException::class) fun parse(string: String): JSON } /** - * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON strings. + * Interface abstracting a JSON serializer that converts [JSON] POJOs into single-line JSON + * strings. */ fun interface JSONSerializer { diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java new file mode 100644 index 0000000..10253d8 --- /dev/null +++ b/sop-java/src/test/java/sop/VerificationJSONTest.java @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; +import sop.enums.SignatureMode; + +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class VerificationJSONTest { + + // A hacky self-made "JSON parser" stand-in. + // Only used for testing, do not use in production! + private Verification.JSONParser dummyParser = new Verification.JSONParser() { + @NotNull + @Override + public Verification.JSON parse(@NotNull String string) throws ParseException { + if (!string.startsWith("{")) { + throw new ParseException("Alleged JSON String does not begin with '{'", 0); + } + if (!string.endsWith("}")) { + throw new ParseException("Alleged JSON String does not end with '}'", string.length() - 1); + } + + List signersList = new ArrayList<>(); + Matcher signersMat = Pattern.compile("\"signers\": \\[(.*?)\\]").matcher(string); + if (signersMat.find()) { + String signersCat = signersMat.group(1); + String[] split = signersCat.split(","); + for (String s : split) { + s = s.trim(); + signersList.add(s.substring(1, s.length() - 1)); + } + } + + String comment = null; + Matcher commentMat = Pattern.compile("\"comment\": \"(.*?)\"").matcher(string); + if (commentMat.find()) { + comment = commentMat.group(1); + } + + String ext = null; + Matcher extMat = Pattern.compile("\"ext\": (.*?})}").matcher(string); + if (extMat.find()) { + ext = extMat.group(1); + } + + return new Verification.JSON(signersList, comment, ext); + } + }; + + // A just as hacky "JSON Serializer" lookalike. + // Also don't use in production, for testing only! + private Verification.JSONSerializer dummySerializer = new Verification.JSONSerializer() { + @NotNull + @Override + public String serialize(@NotNull Verification.JSON json) { + if (json.getSigners().isEmpty() && json.getComment() == null && json.getExt() == null) { + return ""; + } + StringBuilder sb = new StringBuilder("{"); + boolean comma = false; + + if (!json.getSigners().isEmpty()) { + comma = true; + sb.append("\"signers\": ["); + for (Iterator iterator = json.getSigners().iterator(); iterator.hasNext(); ) { + String signer = iterator.next(); + sb.append("\"").append(signer).append("\""); + if (iterator.hasNext()) { + sb.append(", "); + } + } + sb.append("]"); + } + + if (json.getComment() != null) { + if (comma) { + sb.append(", "); + } + comma = true; + sb.append("\"comment\": \"").append(json.getComment()).append("\""); + } + + if (json.getExt() != null) { + if (comma) { + sb.append(", "); + } + comma = true; + sb.append("\"ext\": ").append(json.getExt().toString()); + } + return sb.append("}").toString(); + } + }; + + @Test + public void testSimpleSerializeParse() throws ParseException { + String signer = "alice.pub"; + Verification.JSON json = new Verification.JSON(signer); + + String string = dummySerializer.serialize(json); + assertEquals("{\"signers\": [\"alice.pub\"]}", string); + + Verification.JSON parsed = dummyParser.parse(string); + assertEquals(signer, parsed.getSigners().get(0)); + assertEquals(1, parsed.getSigners().size()); + assertNull(parsed.getComment()); + assertNull(parsed.getExt()); + } + + @Test + public void testAdvancedSerializeParse() throws ParseException { + Verification.JSON json = new Verification.JSON( + Arrays.asList("../certs/alice.pub", "/etc/pgp/certs.pgp"), + "This is a comment", + "{\"Foo\": \"Bar\"}"); + + String serialized = dummySerializer.serialize(json); + assertEquals("{\"signers\": [\"../certs/alice.pub\", \"/etc/pgp/certs.pgp\"], \"comment\": \"This is a comment\", \"ext\": {\"Foo\": \"Bar\"}}", + serialized); + + Verification.JSON parsed = dummyParser.parse(serialized); + assertEquals(json.getSigners(), parsed.getSigners()); + assertEquals(json.getComment(), parsed.getComment()); + assertEquals(json.getExt(), parsed.getExt()); + } + + @Test + public void testVerificationWithSimpleJson() { + String string = "2019-10-29T18:36:45Z EB85BB5FA33A75E15E944E63F231550C4F47E38E EB85BB5FA33A75E15E944E63F231550C4F47E38E mode:text {\"signers\": [\"alice.pgp\"]}"; + Verification verification = Verification.fromString(string); + + assertTrue(verification.getContainsJson()); + assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningKeyFingerprint()); + assertEquals("EB85BB5FA33A75E15E944E63F231550C4F47E38E", verification.getSigningCertFingerprint()); + assertEquals(SignatureMode.text, verification.getSignatureMode().get()); + + Verification.JSON json = verification.getJson(dummyParser); + assertNotNull(json, "The verification string MUST contain valid extension json"); + + assertEquals(Collections.singletonList("alice.pgp"), json.getSigners()); + assertNull(json.getComment()); + assertNull(json.getExt()); + + verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer); + assertEquals(string, verification.toString()); + } +} diff --git a/sop-java/src/test/java/sop/VerificationTest.java b/sop-java/src/test/java/sop/VerificationTest.java index e956435..1e10f61 100644 --- a/sop-java/src/test/java/sop/VerificationTest.java +++ b/sop-java/src/test/java/sop/VerificationTest.java @@ -13,6 +13,7 @@ import java.text.ParseException; import java.util.Date; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; public class VerificationTest { @@ -25,6 +26,8 @@ public class VerificationTest { Verification verification = new Verification(signDate, keyFP, certFP); assertEquals("2022-11-07T15:01:24Z F9E6F53F7201C60A87064EAB0B27F2B0760A1209 4E2C78519512C2AE9A8BFE7EB3298EB2FBE5F51B", verification.toString()); + assertFalse(verification.getContainsJson()); + VerificationAssert.assertThatVerification(verification) .issuedBy(certFP) .isBySigningKey(keyFP) From 21766a1f39f5bfc56553d930047fad607f7c29dd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 3 Jun 2025 23:18:34 +0200 Subject: [PATCH 220/238] Delete ProxyOutputStream and test --- .../main/kotlin/sop/util/ProxyOutputStream.kt | 77 ------------------- .../java/sop/util/ProxyOutputStreamTest.java | 40 ---------- 2 files changed, 117 deletions(-) delete mode 100644 sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt delete mode 100644 sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java diff --git a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt b/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt deleted file mode 100644 index 4ba24b8..0000000 --- a/sop-java/src/main/kotlin/sop/util/ProxyOutputStream.kt +++ /dev/null @@ -1,77 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util - -import java.io.ByteArrayOutputStream -import java.io.IOException -import java.io.OutputStream - -/** - * [OutputStream] that buffers data being written into it, until its underlying output stream is - * being replaced. At that point, first all the buffered data is being written to the underlying - * stream, followed by any successive data that may get written to the [ProxyOutputStream]. This - * 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. - */ -@Deprecated("Marked for removal.") -// TODO: Remove in 11.X -class ProxyOutputStream : OutputStream() { - private val buffer = ByteArrayOutputStream() - private var swapped: OutputStream? = null - - @Synchronized - fun replaceOutputStream(underlying: OutputStream) { - this.swapped = underlying - swapped!!.write(buffer.toByteArray()) - } - - @Synchronized - @Throws(IOException::class) - override fun write(b: ByteArray) { - if (swapped == null) { - buffer.write(b) - } else { - swapped!!.write(b) - } - } - - @Synchronized - @Throws(IOException::class) - override fun write(b: ByteArray, off: Int, len: Int) { - if (swapped == null) { - buffer.write(b, off, len) - } else { - swapped!!.write(b, off, len) - } - } - - @Synchronized - @Throws(IOException::class) - override fun flush() { - buffer.flush() - if (swapped != null) { - swapped!!.flush() - } - } - - @Synchronized - @Throws(IOException::class) - override fun close() { - buffer.close() - if (swapped != null) { - swapped!!.close() - } - } - - @Synchronized - @Throws(IOException::class) - override fun write(i: Int) { - if (swapped == null) { - buffer.write(i) - } else { - swapped!!.write(i) - } - } -} diff --git a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java b/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java deleted file mode 100644 index 9d99fd4..0000000 --- a/sop-java/src/test/java/sop/util/ProxyOutputStreamTest.java +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.util; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -import org.junit.jupiter.api.Test; - -public class ProxyOutputStreamTest { - - @Test - public void replaceOutputStreamThrowsNPEForNull() { - ProxyOutputStream proxy = new ProxyOutputStream(); - assertThrows(NullPointerException.class, () -> proxy.replaceOutputStream(null)); - } - - @Test - public void testSwappingStreamPreservesWrittenBytes() throws IOException { - byte[] firstSection = "Foo\nBar\n".getBytes(StandardCharsets.UTF_8); - byte[] secondSection = "Baz\n".getBytes(StandardCharsets.UTF_8); - - ProxyOutputStream proxy = new ProxyOutputStream(); - proxy.write(firstSection); - - ByteArrayOutputStream swappedStream = new ByteArrayOutputStream(); - proxy.replaceOutputStream(swappedStream); - - proxy.write(secondSection); - proxy.close(); - - assertEquals("Foo\nBar\nBaz\n", swappedStream.toString()); - } -} From 8a7fd5cb58ee1689ed7be2a00f9e08a55cf09985 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:15:29 +0200 Subject: [PATCH 221/238] Move validate-userid to SOPV --- external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt | 4 ++++ sop-java/src/main/kotlin/sop/SOP.kt | 3 --- sop-java/src/main/kotlin/sop/SOPV.kt | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt index f22f947..3341055 100644 --- a/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt +++ b/external-sop/src/main/kotlin/sop/external/ExternalSOPV.kt @@ -10,9 +10,11 @@ import sop.SOPV import sop.external.ExternalSOP.TempDirProvider import sop.external.operation.DetachedVerifyExternal import sop.external.operation.InlineVerifyExternal +import sop.external.operation.ValidateUserIdExternal import sop.external.operation.VersionExternal import sop.operation.DetachedVerify import sop.operation.InlineVerify +import sop.operation.ValidateUserId import sop.operation.Version /** @@ -37,6 +39,8 @@ class ExternalSOPV( override fun inlineVerify(): InlineVerify = InlineVerifyExternal(binaryName, properties, tempDirProvider) + override fun validateUserId(): ValidateUserId = ValidateUserIdExternal(binaryName, properties) + companion object { /** diff --git a/sop-java/src/main/kotlin/sop/SOP.kt b/sop-java/src/main/kotlin/sop/SOP.kt index fbd0428..a942c56 100644 --- a/sop-java/src/main/kotlin/sop/SOP.kt +++ b/sop-java/src/main/kotlin/sop/SOP.kt @@ -68,7 +68,4 @@ interface SOP : SOPV { /** Certify OpenPGP Certificate User-IDs. */ fun certifyUserId(): CertifyUserId? - - /** Validate a UserID in an OpenPGP certificate. */ - fun validateUserId(): ValidateUserId? } diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt index 58a7f13..27eb6e3 100644 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -6,6 +6,7 @@ package sop import sop.operation.DetachedVerify import sop.operation.InlineVerify +import sop.operation.ValidateUserId import sop.operation.Version /** Subset of [SOP] implementing only OpenPGP signature verification. */ @@ -31,4 +32,7 @@ interface SOPV { * a message, use [detachedVerify] instead. */ fun inlineVerify(): InlineVerify? + + /** Validate a UserID in an OpenPGP certificate. */ + fun validateUserId(): ValidateUserId? } From 2a22cea29b52da0abc238a5860c3e251380d3dc2 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:28:28 +0200 Subject: [PATCH 222/238] Remove animalsniffer --- build.gradle | 13 ------------- version.gradle | 1 - 2 files changed, 14 deletions(-) diff --git a/build.gradle b/build.gradle index 1bff704..10f2b87 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,6 @@ buildscript { } plugins { - id 'ru.vyarus.animalsniffer' version '2.0.0' id 'org.jetbrains.kotlin.jvm' version "1.9.21" id 'com.diffplug.spotless' version '6.22.0' apply false } @@ -35,18 +34,6 @@ allprojects { apply plugin: 'kotlin-kapt' apply plugin: 'com.diffplug.spotless' - // For non-cli modules enable android api compatibility check - if (it.name.equals('sop-java')) { - // animalsniffer - apply plugin: 'ru.vyarus.animalsniffer' - dependencies { - signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:2.3.3_r2@signature" - } - animalsniffer { - sourceSets = [sourceSets.main] - } - } - // Only generate jar for submodules // https://stackoverflow.com/a/25445035 jar { diff --git a/version.gradle b/version.gradle index c757e1e..4d90e55 100644 --- a/version.gradle +++ b/version.gradle @@ -6,7 +6,6 @@ allprojects { ext { shortVersion = '11.0.0' isSnapshot = true - minAndroidSdk = 10 javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' From ac17000ff16dc14bdf19768685fa27dbb27f52f4 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:28:58 +0200 Subject: [PATCH 223/238] Clean up unused version literal --- version.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/version.gradle b/version.gradle index 4d90e55..a58af0b 100644 --- a/version.gradle +++ b/version.gradle @@ -10,7 +10,6 @@ allprojects { gsonVersion = '2.10.1' jsrVersion = '3.0.2' junitVersion = '5.8.2' - junitSysExitVersion = '1.1.2' logbackVersion = '1.2.13' // 1.4+ cause CLI spam mockitoVersion = '4.5.1' picocliVersion = '4.6.3' From e72e5a15c0c0a823072ca22f5a4fc57557a83672 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:29:25 +0200 Subject: [PATCH 224/238] Fix: Pass chars to StringBuilder.append() --- sop-java/src/test/java/sop/VerificationJSONTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java index 10253d8..a80e6fc 100644 --- a/sop-java/src/test/java/sop/VerificationJSONTest.java +++ b/sop-java/src/test/java/sop/VerificationJSONTest.java @@ -81,12 +81,12 @@ public class VerificationJSONTest { sb.append("\"signers\": ["); for (Iterator iterator = json.getSigners().iterator(); iterator.hasNext(); ) { String signer = iterator.next(); - sb.append("\"").append(signer).append("\""); + sb.append('\"').append(signer).append('\"'); if (iterator.hasNext()) { sb.append(", "); } } - sb.append("]"); + sb.append(']'); } if (json.getComment() != null) { @@ -94,7 +94,7 @@ public class VerificationJSONTest { sb.append(", "); } comma = true; - sb.append("\"comment\": \"").append(json.getComment()).append("\""); + sb.append("\"comment\": \"").append(json.getComment()).append('\"'); } if (json.getExt() != null) { @@ -104,7 +104,7 @@ public class VerificationJSONTest { comma = true; sb.append("\"ext\": ").append(json.getExt().toString()); } - return sb.append("}").toString(); + return sb.append('}').toString(); } }; From 86718a26905c16af996dd5d4a15530eac4063ced Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:29:53 +0200 Subject: [PATCH 225/238] Bump logback to 1.5.13 --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index a58af0b..864c585 100644 --- a/version.gradle +++ b/version.gradle @@ -10,7 +10,7 @@ allprojects { gsonVersion = '2.10.1' jsrVersion = '3.0.2' junitVersion = '5.8.2' - logbackVersion = '1.2.13' // 1.4+ cause CLI spam + logbackVersion = '1.5.13' mockitoVersion = '4.5.1' picocliVersion = '4.6.3' slf4jVersion = '1.7.36' From 01be696f75c3a48367b4e464a24238dcfa6e6ffd Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 11:30:21 +0200 Subject: [PATCH 226/238] Bump version to 14.0.0-SNAPSHOT --- version.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.gradle b/version.gradle index 864c585..cfb972a 100644 --- a/version.gradle +++ b/version.gradle @@ -4,7 +4,7 @@ allprojects { ext { - shortVersion = '11.0.0' + shortVersion = '14.0.0' isSnapshot = true javaSourceCompatibility = 11 gsonVersion = '2.10.1' From 04d154f63d5e9e797e5dec8b2a80fdfae734e7b8 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 12:12:23 +0200 Subject: [PATCH 227/238] Version: Fix getSopJavaVersion() --- .../src/main/java/sop/testsuite/operation/VersionTest.java | 6 ++++++ sop-java/src/main/kotlin/sop/operation/Version.kt | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java index f836935..47644bf 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java @@ -86,4 +86,10 @@ public class VersionTest extends AbstractSOPTest { throw new TestAbortedException("Implementation does not provide coverage for any sopv interface version."); } } + + @ParameterizedTest + @MethodSource("provideInstances") + public void sopJavaVersionTest(SOP sop) { + assertNotNull(sop.version().getSopJavaVersion()); + } } diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index a10fe7c..6c8aa95 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -115,12 +115,12 @@ interface Version { fun getSopJavaVersion(): String? { return try { val resourceIn: InputStream = - javaClass.getResourceAsStream("/sop-java-version.properties") + Version::class.java.getResourceAsStream("/sop-java-version.properties") ?: throw IOException("File sop-java-version.properties not found.") val properties = Properties().apply { load(resourceIn) } properties.getProperty("sop-java-version") } catch (e: IOException) { - null + "DEVELOPMENT" } } } From 12835bfb8e17538f1ea87f3d8ee3e1abb77d73e9 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 12:12:41 +0200 Subject: [PATCH 228/238] Add/fix missing localizations for new SOP commands --- sop-java-picocli/src/main/resources/msg_update-key.properties | 2 +- .../src/main/resources/msg_update-key_de.properties | 2 +- sop-java-picocli/src/main/resources/msg_version.properties | 1 + sop-java-picocli/src/main/resources/msg_version_de.properties | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sop-java-picocli/src/main/resources/msg_update-key.properties b/sop-java-picocli/src/main/resources/msg_update-key.properties index e12fbbc..0b5243e 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key.properties @@ -4,7 +4,7 @@ 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 +no-added-capabilities=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 diff --git a/sop-java-picocli/src/main/resources/msg_update-key_de.properties b/sop-java-picocli/src/main/resources/msg_update-key_de.properties index 1b8a84d..91e5532 100644 --- a/sop-java-picocli/src/main/resources/msg_update-key_de.properties +++ b/sop-java-picocli/src/main/resources/msg_update-key_de.properties @@ -4,7 +4,7 @@ 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 +no-added-capabilities=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 diff --git a/sop-java-picocli/src/main/resources/msg_version.properties b/sop-java-picocli/src/main/resources/msg_version.properties index c7d0168..1327a78 100644 --- a/sop-java-picocli/src/main/resources/msg_version.properties +++ b/sop-java-picocli/src/main/resources/msg_version.properties @@ -5,6 +5,7 @@ usage.header=Display version information about the tool extended=Print an extended version string backend=Print information about the cryptographic backend sop-spec=Print the latest revision of the SOP specification targeted by the implementation +sopv=Print the SOPV API version standardOutput=version information diff --git a/sop-java-picocli/src/main/resources/msg_version_de.properties b/sop-java-picocli/src/main/resources/msg_version_de.properties index c317916..c99045c 100644 --- a/sop-java-picocli/src/main/resources/msg_version_de.properties +++ b/sop-java-picocli/src/main/resources/msg_version_de.properties @@ -5,6 +5,7 @@ usage.header=Zeige Versionsinformationen extended=Gebe erweiterte Versionsinformationen 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 +sopv=Gebe die SOPV API Version aus standardOutput=Versionsinformationen From 07d0aa69416c65b53fb6cf8d9bbfddd7525e8395 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 12:24:15 +0200 Subject: [PATCH 229/238] Update CHANGELOG --- CHANGELOG.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0500cb..9271f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,22 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 14.0.0-SNAPSHOT +- Implement changes from SOP spec `11`, `12`, `13`, `14` + - Implement `update-key` command + - Implement `merge-certs` command + - Implement `certify-userid` command + - Implement `validate-userid` command + - Add `UnspecificFailure` exception + - Add `KeyCannotCertify` exception + - Add `NoHardwareKeyFound` exception + - Add `HardwareKeyFailure` exception + - Add `PrimaryKeyBad` exception + - Add `CertUserIdNoMatch` exception + - `Verification`: Add support for JSON description extensions +- Remove `animalsniffer` from build dependencies +- Bump `logback` to `1.5.13` + ## 10.1.1 - Prepare jar files for use in native images, e.g. using GraalVM by generating and including configuration files for reflection, resources and dynamic proxies. From 191ec8c07d8cfe7bf3c18e887379d40baae65f82 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 13:22:16 +0200 Subject: [PATCH 230/238] Improve CHANGELOG again --- CHANGELOG.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9271f3d..3f32b28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,17 +7,20 @@ SPDX-License-Identifier: Apache-2.0 # Changelog ## 14.0.0-SNAPSHOT -- Implement changes from SOP spec `11`, `12`, `13`, `14` - - Implement `update-key` command - - Implement `merge-certs` command - - Implement `certify-userid` command - - Implement `validate-userid` command - - Add `UnspecificFailure` exception - - Add `KeyCannotCertify` exception - - Add `NoHardwareKeyFound` exception - - Add `HardwareKeyFailure` exception - - Add `PrimaryKeyBad` exception - - Add `CertUserIdNoMatch` exception +- Update implementation to [SOP Specification revision 14](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-14.html), + including changes from revisions `11`, `12`, `13`, `14`. + - Implement newly introduced operations + - `update-key` 'fixes' everything wrong with a key + - `merge-certs` merges a certificate with other copies + - `certify-userid` create signatures over user-ids on certificates + - `validate-userid` validate signatures over user-ids + - Add new exceptions + - `UnspecificFailure` maps generic application errors + - `KeyCannotCertify` signals that a key cannot be used for third-party certifications + - `NoHardwareKeyFound` signals that a key backed by a hardware device cannot be found + - `HardwareKeyFailure` signals a hardware device failure + - `PrimaryKeyBad` signals an unusable or bad primary key + - `CertUserIdNoMatch` signals that a user-id cannot be found/validated on a certificate - `Verification`: Add support for JSON description extensions - Remove `animalsniffer` from build dependencies - Bump `logback` to `1.5.13` From 9762f1f043546a260f901e16f30b4d0daaa243fb Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 13:39:23 +0200 Subject: [PATCH 231/238] SOP-Java 14.0.0 --- CHANGELOG.md | 2 +- README.md | 2 +- version.gradle | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f32b28..0447a9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ SPDX-License-Identifier: Apache-2.0 # Changelog -## 14.0.0-SNAPSHOT +## 14.0.0 - Update implementation to [SOP Specification revision 14](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-14.html), including changes from revisions `11`, `12`, `13`, `14`. - Implement newly introduced operations diff --git a/README.md b/README.md index f2e207f..35324c4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0 # SOP for Java [![status-badge](https://ci.codeberg.org/api/badges/PGPainless/sop-java/status.svg)](https://ci.codeberg.org/PGPainless/sop-java) -[![Spec Revision: 10](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/10/) +[![Spec Revision: 14](https://img.shields.io/badge/Spec%20Revision-10-blue)](https://datatracker.ietf.org/doc/draft-dkg-openpgp-stateless-cli/14/) [![Coverage Status](https://coveralls.io/repos/github/pgpainless/sop-java/badge.svg?branch=main)](https://coveralls.io/github/pgpainless/sop-java?branch=main) [![REUSE status](https://api.reuse.software/badge/github.com/pgpainless/sop-java)](https://api.reuse.software/info/github.com/pgpainless/sop-java) diff --git a/version.gradle b/version.gradle index cfb972a..cb9b332 100644 --- a/version.gradle +++ b/version.gradle @@ -5,7 +5,7 @@ allprojects { ext { shortVersion = '14.0.0' - isSnapshot = true + isSnapshot = false javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' From b3223372c690aa7f8277ce226c34e6ad314d7d68 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 17 Jun 2025 13:42:42 +0200 Subject: [PATCH 232/238] SOP-Java 14.0.1-SNAPSHOT --- version.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.gradle b/version.gradle index cb9b332..bac96da 100644 --- a/version.gradle +++ b/version.gradle @@ -4,8 +4,8 @@ allprojects { ext { - shortVersion = '14.0.0' - isSnapshot = false + shortVersion = '14.0.1' + isSnapshot = true javaSourceCompatibility = 11 gsonVersion = '2.10.1' jsrVersion = '3.0.2' From c651adc0b391e764161f09e0eb0869985d468058 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Jul 2025 14:38:13 +0200 Subject: [PATCH 233/238] Add VerificationAssert methods, disable tests for unsupported operations --- .../assertions/VerificationAssert.java | 25 ++++++ .../testsuite/operation/AbstractSOPTest.java | 16 ++++ .../testsuite/operation/ArmorDearmorTest.java | 40 ++++----- .../operation/CertifyValidateUserIdTest.java | 58 ++++++------- .../operation/ChangeKeyPasswordTest.java | 36 ++++---- .../operation/DecryptWithSessionKeyTest.java | 4 +- .../DetachedSignDetachedVerifyTest.java | 48 +++++------ .../operation/EncryptDecryptTest.java | 83 ++++++++++--------- .../testsuite/operation/ExtractCertTest.java | 22 ++--- .../testsuite/operation/GenerateKeyTest.java | 22 ++--- ...ineSignInlineDetachDetachedVerifyTest.java | 14 ++-- .../operation/InlineSignInlineVerifyTest.java | 33 ++++---- .../testsuite/operation/ListProfilesTest.java | 9 +- .../testsuite/operation/MergeCertsTest.java | 44 +++++----- .../testsuite/operation/RevokeKeyTest.java | 46 +++++----- .../sop/testsuite/operation/VersionTest.java | 22 ++--- .../test/java/sop/VerificationJSONTest.java | 3 + 17 files changed, 286 insertions(+), 239 deletions(-) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java index 5267148..dea8717 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/assertions/VerificationAssert.java @@ -8,9 +8,13 @@ import sop.Verification; import sop.enums.SignatureMode; import sop.testsuite.JUtils; +import java.text.ParseException; import java.util.Date; +import java.util.function.Predicate; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; public final class VerificationAssert { @@ -57,6 +61,27 @@ public final class VerificationAssert { return hasDescription(description); } + public VerificationAssert hasValidJSONOrNull(Verification.JSONParser parser) + throws ParseException { + if (!verification.getJsonOrDescription().isPresent()) { + // missing description + return this; + } + + return hasJSON(parser, null); + } + + public VerificationAssert hasJSON(Verification.JSONParser parser, Predicate predicate) { + assertTrue(verification.getContainsJson(), "Verification does not appear to contain JSON extension"); + + Verification.JSON json = verification.getJson(parser); + assertNotNull(verification.getJson(parser), "Verification does not appear to contain valid JSON extension."); + if (predicate != null) { + assertTrue(predicate.test(json), "JSON object does not match predicate."); + } + return this; + } + public VerificationAssert hasMode(SignatureMode mode) { assertEquals(mode, verification.getSignatureMode().get()); return this; diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java index 6c163f7..16ae256 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/AbstractSOPTest.java @@ -4,10 +4,13 @@ package sop.testsuite.operation; +import kotlin.jvm.functions.Function0; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Named; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.provider.Arguments; import sop.SOP; +import sop.exception.SOPGPException; import sop.testsuite.AbortOnUnsupportedOption; import sop.testsuite.AbortOnUnsupportedOptionExtension; import sop.testsuite.SOPInstanceFactory; @@ -18,6 +21,8 @@ import java.util.List; import java.util.Map; import java.util.stream.Stream; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + @ExtendWith(AbortOnUnsupportedOptionExtension.class) @AbortOnUnsupportedOption public abstract class AbstractSOPTest { @@ -51,6 +56,17 @@ public abstract class AbstractSOPTest { } } + public T assumeSupported(Function0 f) { + try { + T t = f.invoke(); + assumeTrue(t != null, "Unsupported operation."); + return t; + } catch (SOPGPException.UnsupportedSubcommand e) { + assumeTrue(false, e.getMessage()); + return null; + } + } + public static Stream provideBackends() { return backends.stream(); } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java index 35959b0..00488e1 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ArmorDearmorTest.java @@ -20,7 +20,7 @@ import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ArmorDearmorTest { +public class ArmorDearmorTest extends AbstractSOPTest { static Stream provideInstances() { return AbstractSOPTest.provideBackends(); @@ -31,13 +31,13 @@ public class ArmorDearmorTest { public void dearmorArmorAliceKey(SOP sop) throws IOException { byte[] aliceKey = TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(aliceKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -52,13 +52,13 @@ public class ArmorDearmorTest { public void dearmorArmorAliceCert(SOP sop) throws IOException { byte[] aliceCert = TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(aliceCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -73,13 +73,13 @@ public class ArmorDearmorTest { public void dearmorArmorBobKey(SOP sop) throws IOException { byte[] bobKey = TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(bobKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -94,13 +94,13 @@ public class ArmorDearmorTest { public void dearmorArmorBobCert(SOP sop) throws IOException { byte[] bobCert = TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(bobCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -115,13 +115,13 @@ public class ArmorDearmorTest { public void dearmorArmorCarolKey(SOP sop) throws IOException { byte[] carolKey = TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(carolKey) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -136,13 +136,13 @@ public class ArmorDearmorTest { public void dearmorArmorCarolCert(SOP sop) throws IOException { byte[] carolCert = TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(carolCert) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -163,13 +163,13 @@ public class ArmorDearmorTest { "CePQFpprprnGEzpE3flQLUc=\n" + "=ZiFR\n" + "-----END PGP MESSAGE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(message) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_MESSAGE)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -191,13 +191,13 @@ public class ArmorDearmorTest { "=GHvQ\n" + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(signature) .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(dearmored, TestData.BEGIN_PGP_SIGNATURE)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(dearmored) .getBytes(); @@ -210,11 +210,11 @@ public class ArmorDearmorTest { @ParameterizedTest @MethodSource("provideInstances") public void testDearmoringTwiceIsIdempotent(SOP sop) throws IOException { - byte[] dearmored = sop.dearmor() + byte[] dearmored = assumeSupported(sop::dearmor) .data(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); - byte[] dearmoredAgain = sop.dearmor() + byte[] dearmoredAgain = assumeSupported(sop::dearmor) .data(dearmored) .getBytes(); @@ -233,7 +233,7 @@ public class ArmorDearmorTest { "=GHvQ\n" + "-----END PGP SIGNATURE-----\n").getBytes(StandardCharsets.UTF_8); - byte[] armoredAgain = sop.armor() + byte[] armoredAgain = assumeSupported(sop::armor) .data(armored) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java index 7f9f088..855c23d 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/CertifyValidateUserIdTest.java @@ -18,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class CertifyValidateUserIdTest { +public class CertifyValidateUserIdTest extends AbstractSOPTest { static Stream provideInstances() { return AbstractSOPTest.provideBackends(); @@ -27,25 +27,25 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void certifyUserId(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .withKeyPassword("sw0rdf1sh") .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); // Alice has her own user-id self-certified - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .authorities(aliceCert) .userId("Alice ") .subjects(aliceCert), @@ -53,20 +53,20 @@ public class CertifyValidateUserIdTest { // Alice has not yet certified Bobs user-id assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.validateUserId() + assumeSupported(sop::validateUserId) .authorities(aliceCert) .userId("Bob ") .subjects(bobCert), "Alice has not yet certified Bobs user-id"); - byte[] bobCertifiedByAlice = sop.certifyUserId() + byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId) .userId("Bob ") .withKeyPassword("sw0rdf1sh") .keys(aliceKey) .certs(bobCert) .getBytes(); - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .userId("Bob ") .authorities(aliceCert) .subjects(bobCertifiedByAlice), @@ -76,28 +76,28 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void certifyUserIdUnarmored(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .noArmor() .withKeyPassword("sw0rdf1sh") .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .noArmor() .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .noArmor() .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .noArmor() .key(bobKey) .getBytes(); - byte[] bobCertifiedByAlice = sop.certifyUserId() + byte[] bobCertifiedByAlice = assumeSupported(sop::certifyUserId) .noArmor() .userId("Bob ") .withKeyPassword("sw0rdf1sh") @@ -105,7 +105,7 @@ public class CertifyValidateUserIdTest { .certs(bobCert) .getBytes(); - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .userId("Bob ") .authorities(aliceCert) .subjects(bobCertifiedByAlice), @@ -115,45 +115,45 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void addPetName(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.certifyUserId() + assumeSupported(sop::certifyUserId) .userId("Bobby") .keys(aliceKey) .certs(bobCert) .getBytes(), "Alice cannot create a pet-name for Bob without the --no-require-self-sig flag"); - byte[] bobWithPetName = sop.certifyUserId() + byte[] bobWithPetName = assumeSupported(sop::certifyUserId) .userId("Bobby") .noRequireSelfSig() .keys(aliceKey) .certs(bobCert) .getBytes(); - assertTrue(sop.validateUserId() + assertTrue(assumeSupported(sop::validateUserId) .userId("Bobby") .authorities(aliceCert) .subjects(bobWithPetName), "Alice accepts the pet-name she gave to Bob"); assertThrows(SOPGPException.CertUserIdNoMatch.class, () -> - sop.validateUserId() + assumeSupported(sop::validateUserId) .userId("Bobby") .authorities(bobWithPetName) .subjects(bobWithPetName), @@ -163,28 +163,28 @@ public class CertifyValidateUserIdTest { @ParameterizedTest @MethodSource("provideInstances") public void certifyWithRevokedKey(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] aliceRevokedCert = sop.revokeKey() + byte[] aliceRevokedCert = assumeSupported(sop::revokeKey) .keys(aliceKey) .getBytes(); - byte[] aliceRevokedKey = sop.updateKey() + byte[] aliceRevokedKey = assumeSupported(sop::updateKey) .mergeCerts(aliceRevokedCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); assertThrows(SOPGPException.KeyCannotCertify.class, () -> - sop.certifyUserId() + assumeSupported(sop::certifyUserId) .userId("Bob ") .keys(aliceRevokedKey) .certs(bobCert) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java index 8948dda..a62cbb8 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ChangeKeyPasswordTest.java @@ -32,18 +32,18 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void changePasswordFromUnprotectedToProtected(SOP sop) throws IOException { - byte[] unprotectedKey = sop.generateKey().generate().getBytes(); + byte[] unprotectedKey = assumeSupported(sop::generateKey).generate().getBytes(); byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.changeKeyPassword().newKeyPassphrase(password).keys(unprotectedKey).getBytes(); + byte[] protectedKey = assumeSupported(sop::changeKeyPassword).newKeyPassphrase(password).keys(unprotectedKey).getBytes(); - sop.sign().withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + assumeSupported(sop::sign).withKeyPassword(password).key(protectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); } @ParameterizedTest @MethodSource("provideInstances") public void changePasswordFromUnprotectedToUnprotected(SOP sop) throws IOException { - byte[] unprotectedKey = sop.generateKey().noArmor().generate().getBytes(); - byte[] stillUnprotectedKey = sop.changeKeyPassword().noArmor().keys(unprotectedKey).getBytes(); + byte[] unprotectedKey = assumeSupported(sop::generateKey).noArmor().generate().getBytes(); + byte[] stillUnprotectedKey = assumeSupported(sop::changeKeyPassword).noArmor().keys(unprotectedKey).getBytes(); assertArrayEquals(unprotectedKey, stillUnprotectedKey); } @@ -52,12 +52,12 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @MethodSource("provideInstances") public void changePasswordFromProtectedToUnprotected(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(password).generate().getBytes(); - byte[] unprotectedKey = sop.changeKeyPassword() + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(password).generate().getBytes(); + byte[] unprotectedKey = assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(password) .keys(protectedKey).getBytes(); - sop.sign().key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); + assumeSupported(sop::sign).key(unprotectedKey).data("Test123".getBytes(StandardCharsets.UTF_8)); } @ParameterizedTest @@ -65,13 +65,13 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { public void changePasswordFromProtectedToDifferentProtected(SOP sop) throws IOException { byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); - byte[] reprotectedKey = sop.changeKeyPassword() + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); + byte[] reprotectedKey = assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(oldPassword) .newKeyPassphrase(newPassword) .keys(protectedKey).getBytes(); - sop.sign().key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); + assumeSupported(sop::sign).key(reprotectedKey).withKeyPassword(newPassword).data("Test123".getBytes(StandardCharsets.UTF_8)); } @@ -82,8 +82,8 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { byte[] newPassword = "monkey123".getBytes(UTF8Util.UTF8); byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.changeKeyPassword() + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(wrongPassword) .newKeyPassphrase(newPassword) .keys(protectedKey).getBytes()); @@ -93,9 +93,9 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { @MethodSource("provideInstances") public void nonUtf8PasswordsFail(SOP sop) { assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> - sop.changeKeyPassword().oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + assumeSupported(sop::changeKeyPassword).oldKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); assertThrows(SOPGPException.PasswordNotHumanReadable.class, () -> - sop.changeKeyPassword().newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); + assumeSupported(sop::changeKeyPassword).newKeyPassphrase(new byte[] {(byte) 0xff, (byte) 0xfe})); } @@ -104,16 +104,16 @@ public class ChangeKeyPasswordTest extends AbstractSOPTest { public void testNoArmor(SOP sop) throws IOException { byte[] oldPassword = "sw0rdf1sh".getBytes(UTF8Util.UTF8); byte[] newPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] protectedKey = sop.generateKey().withKeyPassword(oldPassword).generate().getBytes(); + byte[] protectedKey = assumeSupported(sop::generateKey).withKeyPassword(oldPassword).generate().getBytes(); - byte[] armored = sop.changeKeyPassword() + byte[] armored = assumeSupported(sop::changeKeyPassword) .oldKeyPassphrase(oldPassword) .newKeyPassphrase(newPassword) .keys(protectedKey) .getBytes(); JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_PRIVATE_KEY_BLOCK); - byte[] unarmored = sop.changeKeyPassword() + byte[] unarmored = assumeSupported(sop::changeKeyPassword) .noArmor() .oldKeyPassphrase(oldPassword) .newKeyPassphrase(newPassword) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java index 65ec4a5..8fd201a 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DecryptWithSessionKeyTest.java @@ -41,7 +41,7 @@ public class DecryptWithSessionKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testDecryptAndExtractSessionKey(SOP sop) throws IOException { - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult(); @@ -54,7 +54,7 @@ public class DecryptWithSessionKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testDecryptWithSessionKey(SOP sop) throws IOException { - byte[] decrypted = sop.decrypt() + byte[] decrypted = assumeSupported(sop::decrypt) .withSessionKey(SessionKey.fromString(SESSION_KEY)) .ciphertext(CIPHERTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java index e404599..415b9db 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/DetachedSignDetachedVerifyTest.java @@ -37,13 +37,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithAliceKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -60,14 +60,14 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyTextModeWithAliceKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(SignAs.text) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -85,7 +85,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -101,13 +101,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithBobKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -123,13 +123,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithCarolKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -145,7 +145,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signVerifyWithEncryptedKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .withKeyPassword(TestData.PASSWORD) .data(message) @@ -154,7 +154,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { JUtils.assertArrayStartsWith(signature, TestData.BEGIN_PGP_SIGNATURE); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -170,18 +170,18 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { public void signArmorVerifyWithBobKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.detachedSign() + byte[] signature = assumeSupported(sop::detachedSign) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .noArmor() .data(message) .toByteArrayAndResult() .getBytes(); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(signature) .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(armored) .data(message); @@ -199,7 +199,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); Date beforeSignature = new Date(TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE.getTime() - 1000); // 1 sec before sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .notAfter(beforeSignature) .signatures(signature) @@ -213,7 +213,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] signature = TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); Date afterSignature = new Date(TestData.ALICE_DETACHED_SIGNED_MESSAGE_DATE.getTime() + 1000); // 1 sec after sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .notBefore(afterSignature) .signatures(signature) @@ -224,13 +224,13 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void signWithAliceVerifyWithBobThrowsNoSignature(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signatures = sop.detachedSign() + byte[] signatures = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> sop.detachedVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::detachedVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signatures) .data(message)); @@ -240,7 +240,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void signVerifyWithEncryptedKeyWithoutPassphraseFails(SOP sop) { assertThrows(SOPGPException.KeyIsProtected.class, () -> - sop.detachedSign() + assumeSupported(sop::detachedSign) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .data(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() @@ -253,7 +253,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signature = sop.sign() + byte[] signature = assumeSupported(sop::sign) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .withKeyPassword("wrong") .withKeyPassword(TestData.PASSWORD) // correct @@ -262,7 +262,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - List verificationList = sop.verify() + List verificationList = assumeSupported(sop::verify) .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signature) .data(message); @@ -279,7 +279,7 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); assertThrows(SOPGPException.MissingArg.class, () -> - sop.verify() + assumeSupported(sop::verify) .signatures(TestData.ALICE_DETACHED_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8)) .data(message)); } @@ -288,14 +288,14 @@ public class DetachedSignDetachedVerifyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void signVerifyWithMultipleKeys(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signatures = sop.detachedSign() + byte[] signatures = assumeSupported(sop::detachedSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult() .getBytes(); - List verificationList = sop.detachedVerify() + List verificationList = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signatures) diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java index 330ca90..937b5b7 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/EncryptDecryptTest.java @@ -49,7 +49,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - ByteArrayAndResult encResult = sop.encrypt() + ByteArrayAndResult encResult = assumeSupported(sop::encrypt) .withPassword("sw0rdf1sh") .plaintext(message) .toByteArrayAndResult(); @@ -57,7 +57,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = encResult.getBytes(); Optional encSessionKey = encResult.getResult().getSessionKey(); - ByteArrayAndResult decResult = sop.decrypt() + ByteArrayAndResult decResult = assumeSupported(sop::decrypt) .withPassword("sw0rdf1sh") .ciphertext(ciphertext) .toByteArrayAndResult(); @@ -65,9 +65,10 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] plaintext = decResult.getBytes(); Optional decSessionKey = decResult.getResult().getSessionKey(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); if (encSessionKey.isPresent() && decSessionKey.isPresent()) { - assertEquals(encSessionKey.get(), decSessionKey.get()); + assertEquals(encSessionKey.get(), decSessionKey.get(), + "Extracted Session Key mismatch."); } } @@ -75,91 +76,93 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptDecryptRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); DecryptionResult result = bytesAndResult.getResult(); - assertNotNull(result.getSessionKey().get()); + if (result.getSessionKey().isPresent()) { + assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); + } } @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptRoundTripBobTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) .toByteArrayAndResult() .getBytes(); - byte[] plaintext = sop.decrypt() + byte[] plaintext = assumeSupported(sop::decrypt) .withKey(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult() .getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); } @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptRoundTripCarolTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .plaintext(message) .toByteArrayAndResult() .getBytes(); - byte[] plaintext = sop.decrypt() + byte[] plaintext = assumeSupported(sop::decrypt) .withKey(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult() .getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); } @ParameterizedTest @MethodSource("provideInstances") public void encryptNoArmorThenArmorThenDecryptRoundTrip(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .noArmor() .plaintext(message) .toByteArrayAndResult() .getBytes(); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(ciphertext) .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .ciphertext(armored) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); } @ParameterizedTest @MethodSource("provideInstances") public void encryptSignDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.binary) @@ -167,17 +170,19 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); DecryptionResult result = bytesAndResult.getResult(); - assertNotNull(result.getSessionKey().get()); + if (result.getSessionKey().isPresent()) { + assertNotNull(result.getSessionKey().get(), "Session key MUST NOT be null."); + } List verificationList = result.getVerifications(); VerificationListAssert.assertThatVerificationList(verificationList) @@ -191,7 +196,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptSignAsTextDecryptVerifyRoundTripAliceTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(EncryptAs.text) @@ -199,14 +204,14 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .ciphertext(ciphertext) .toByteArrayAndResult(); byte[] plaintext = bytesAndResult.getBytes(); - assertArrayEquals(message, plaintext); + assertArrayEquals(message, plaintext, "Decrypted plaintext does not match original plaintext."); DecryptionResult result = bytesAndResult.getResult(); assertNotNull(result.getSessionKey().get()); @@ -222,17 +227,17 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void encryptSignDecryptVerifyRoundTripWithFreshEncryptedKeyTest(SOP sop) throws IOException { byte[] keyPassword = "sw0rdf1sh".getBytes(StandardCharsets.UTF_8); - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .withKeyPassword(keyPassword) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - byte[] ciphertext = sop.encrypt() + byte[] ciphertext = assumeSupported(sop::encrypt) .withCert(cert) .signWith(key) .withKeyPassword(keyPassword) @@ -240,7 +245,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult() .getBytes(); - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(key) .withKeyPassword(keyPassword) .verifyWithCert(cert) @@ -273,7 +278,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before signing date assertThrows(SOPGPException.NoSignature.class, () -> { - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .verifyNotAfter(beforeSignature) @@ -307,7 +312,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec after signing date assertThrows(SOPGPException.NoSignature.class, () -> { - ByteArrayAndResult bytesAndResult = sop.decrypt() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::decrypt) .withKey(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .verifyWithCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .verifyNotBefore(afterSignature) @@ -326,7 +331,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { public void missingArgsTest(SOP sop) { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt() + assertThrows(SOPGPException.MissingArg.class, () -> assumeSupported(sop::encrypt) .plaintext(message) .toByteArrayAndResult() .getBytes()); @@ -336,7 +341,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { @MethodSource("provideInstances") public void passingSecretKeysForPublicKeysFails(SOP sop) { assertThrows(SOPGPException.BadData.class, () -> - sop.encrypt() + assumeSupported(sop::encrypt) .withCert(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() @@ -346,19 +351,19 @@ public class EncryptDecryptTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void encryptDecryptWithAllSupportedKeyGenerationProfiles(SOP sop) throws IOException { - List profiles = sop.listProfiles().generateKey(); + List profiles = assumeSupported(sop::listProfiles).generateKey(); List keys = new ArrayList<>(); List certs = new ArrayList<>(); for (Profile p : profiles) { - byte[] k = sop.generateKey() + byte[] k = assumeSupported(sop::generateKey) .profile(p) .userId(p.getName()) .generate() .getBytes(); keys.add(k); - byte[] c = sop.extractCert() + byte[] c = assumeSupported(sop::extractCert) .key(k) .getBytes(); certs.add(c); @@ -366,7 +371,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] plaintext = "Hello, World!\n".getBytes(); - Encrypt encrypt = sop.encrypt(); + Encrypt encrypt = assumeSupported(sop::encrypt); for (byte[] c : certs) { encrypt.withCert(c); } @@ -380,7 +385,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { byte[] ciphertext = encRes.getBytes(); for (byte[] k : keys) { - Decrypt decrypt = sop.decrypt() + Decrypt decrypt = assumeSupported(sop::decrypt) .withKey(k); for (byte[] c : certs) { decrypt.verifyWithCert(c); @@ -389,7 +394,7 @@ public class EncryptDecryptTest extends AbstractSOPTest { .toByteArrayAndResult(); DecryptionResult dResult = decRes.getResult(); byte[] decPlaintext = decRes.getBytes(); - assertArrayEquals(plaintext, decPlaintext); + assertArrayEquals(plaintext, decPlaintext, "Decrypted plaintext does not match original plaintext."); assertEquals(certs.size(), dResult.getVerifications().size()); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java index 99acf81..94d9927 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ExtractCertTest.java @@ -28,12 +28,12 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractArmoredCertFromArmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getInputStream(); - byte[] cert = sop.extractCert().key(keyIn).getBytes(); + byte[] cert = assumeSupported(sop::extractCert).key(keyIn).getBytes(); JUtils.assertArrayStartsWith(cert, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK); JUtils.assertArrayEndsWithIgnoreNewlines(cert, TestData.END_PGP_PUBLIC_KEY_BLOCK); } @@ -41,7 +41,7 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractAliceCertFromAliceKeyTest(SOP sop) throws IOException { - byte[] armoredCert = sop.extractCert() + byte[] armoredCert = assumeSupported(sop::extractCert) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); JUtils.assertAsciiArmorEquals(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); @@ -50,7 +50,7 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractBobsCertFromBobsKeyTest(SOP sop) throws IOException { - byte[] armoredCert = sop.extractCert() + byte[] armoredCert = assumeSupported(sop::extractCert) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); JUtils.assertAsciiArmorEquals(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); @@ -59,7 +59,7 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractCarolsCertFromCarolsKeyTest(SOP sop) throws IOException { - byte[] armoredCert = sop.extractCert() + byte[] armoredCert = assumeSupported(sop::extractCert) .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .getBytes(); JUtils.assertAsciiArmorEquals(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8), armoredCert); @@ -68,12 +68,12 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractUnarmoredCertFromArmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getInputStream(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .noArmor() .key(keyIn) .getBytes(); @@ -84,13 +84,13 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractArmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .userId("Alice ") .noArmor() .generate() .getInputStream(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(keyIn) .getBytes(); @@ -101,13 +101,13 @@ public class ExtractCertTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void extractUnarmoredCertFromUnarmoredKeyTest(SOP sop) throws IOException { - InputStream keyIn = sop.generateKey() + InputStream keyIn = assumeSupported(sop::generateKey) .noArmor() .userId("Alice ") .generate() .getInputStream(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .noArmor() .key(keyIn) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java index 0d8b78e..787cf62 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/GenerateKeyTest.java @@ -33,7 +33,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); @@ -45,7 +45,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyNoArmor(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .noArmor() .generate() @@ -57,7 +57,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithMultipleUserIdsTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .userId("Bob ") .generate() @@ -70,7 +70,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithoutUserIdTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .generate() .getBytes(); @@ -81,7 +81,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithPasswordTest(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .withKeyPassword("sw0rdf1sh") .generate() @@ -94,7 +94,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithMultipleUserIdsAndPassword(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .userId("Bob ") .withKeyPassword("sw0rdf1sh") @@ -108,17 +108,17 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateSigningOnlyKey(SOP sop) throws IOException { - byte[] signingOnlyKey = sop.generateKey() + byte[] signingOnlyKey = assumeSupported(sop::generateKey) .signingOnly() .userId("Alice ") .generate() .getBytes(); - byte[] signingOnlyCert = sop.extractCert() + byte[] signingOnlyCert = assumeSupported(sop::extractCert) .key(signingOnlyKey) .getBytes(); assertThrows(SOPGPException.CertCannotEncrypt.class, () -> - sop.encrypt().withCert(signingOnlyCert) + assumeSupported(sop::encrypt).withCert(signingOnlyCert) .plaintext(TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8)) .toByteArrayAndResult() .getBytes()); @@ -127,7 +127,7 @@ public class GenerateKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void generateKeyWithSupportedProfiles(SOP sop) throws IOException { - List profiles = sop.listProfiles() + List profiles = assumeSupported(sop::listProfiles) .generateKey(); for (Profile profile : profiles) { @@ -138,7 +138,7 @@ public class GenerateKeyTest extends AbstractSOPTest { private void generateKeyWithProfile(SOP sop, String profile) throws IOException { byte[] key; try { - key = sop.generateKey() + key = assumeSupported(sop::generateKey) .profile(profile) .userId("Alice ") .generate() diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java index 3e20a09..ac043b3 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineDetachDetachedVerifyTest.java @@ -36,12 +36,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { public void inlineSignThenDetachThenDetachedVerifyTest(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); - ByteArrayAndResult bytesAndResult = sop.inlineDetach() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) .message(inlineSigned) .toByteArrayAndResult(); @@ -51,7 +51,7 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { byte[] signatures = bytesAndResult.getResult() .getBytes(); - List verifications = sop.detachedVerify() + List verifications = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(signatures) .data(plaintext); @@ -64,12 +64,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { public void inlineSignThenDetachNoArmorThenArmorThenDetachedVerifyTest(SOP sop) throws IOException { byte[] message = "Hello, World!\n".getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); - ByteArrayAndResult bytesAndResult = sop.inlineDetach() + ByteArrayAndResult bytesAndResult = assumeSupported(sop::inlineDetach) .noArmor() .message(inlineSigned) .toByteArrayAndResult(); @@ -81,12 +81,12 @@ public class InlineSignInlineDetachDetachedVerifyTest extends AbstractSOPTest { .getBytes(); Assertions.assertFalse(JUtils.arrayStartsWith(signatures, TestData.BEGIN_PGP_SIGNATURE)); - byte[] armored = sop.armor() + byte[] armored = assumeSupported(sop::armor) .data(signatures) .getBytes(); JUtils.assertArrayStartsWith(armored, TestData.BEGIN_PGP_SIGNATURE); - List verifications = sop.detachedVerify() + List verifications = assumeSupported(sop::detachedVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .signatures(armored) .data(plaintext); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java index 39a26c6..d751ee8 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/InlineSignInlineVerifyTest.java @@ -40,14 +40,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyAlice(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -66,7 +66,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyAliceNoArmor(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .noArmor() .data(message) @@ -74,7 +74,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { Assertions.assertFalse(JUtils.arrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE)); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -93,7 +93,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void clearsignVerifyAlice(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] clearsigned = sop.inlineSign() + byte[] clearsigned = assumeSupported(sop::inlineSign) .key(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .mode(InlineSignAs.clearsigned) .data(message) @@ -101,12 +101,13 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { JUtils.assertArrayStartsWith(clearsigned, TestData.BEGIN_PGP_SIGNED_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(clearsigned) .toByteArrayAndResult(); - assertArrayEquals(message, bytesAndResult.getBytes()); + assertArrayEquals(message, bytesAndResult.getBytes(), + "ASCII armored message does not appear to start with the 'BEGIN PGP SIGNED MESSAGE' header."); List verificationList = bytesAndResult.getResult(); VerificationListAssert.assertThatVerificationList(verificationList) @@ -121,7 +122,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { byte[] message = TestData.ALICE_INLINE_SIGNED_MESSAGE.getBytes(StandardCharsets.UTF_8); Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE; - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) .toByteArrayAndResult(); @@ -141,7 +142,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE; Date afterSignature = new Date(signatureDate.getTime() + 1000); // 1 sec before sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify) .notBefore(afterSignature) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) @@ -155,7 +156,7 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { Date signatureDate = TestData.ALICE_INLINE_SIGNED_MESSAGE_DATE; Date beforeSignature = new Date(signatureDate.getTime() - 1000); // 1 sec before sig - assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify() + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify) .notAfter(beforeSignature) .cert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .data(message) @@ -167,14 +168,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyBob(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.BOB_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -193,14 +194,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyCarol(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .key(TestData.CAROL_KEY.getBytes(StandardCharsets.UTF_8)) .data(message) .getBytes(); JUtils.assertArrayStartsWith(inlineSigned, TestData.BEGIN_PGP_MESSAGE); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); @@ -219,14 +220,14 @@ public class InlineSignInlineVerifyTest extends AbstractSOPTest { public void inlineSignVerifyProtectedKey(SOP sop) throws IOException { byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] inlineSigned = sop.inlineSign() + byte[] inlineSigned = assumeSupported(sop::inlineSign) .withKeyPassword(TestData.PASSWORD) .key(TestData.PASSWORD_PROTECTED_KEY.getBytes(StandardCharsets.UTF_8)) .mode(InlineSignAs.binary) .data(message) .getBytes(); - ByteArrayAndResult> bytesAndResult = sop.inlineVerify() + ByteArrayAndResult> bytesAndResult = assumeSupported(sop::inlineVerify) .cert(TestData.PASSWORD_PROTECTED_CERT.getBytes(StandardCharsets.UTF_8)) .data(inlineSigned) .toByteArrayAndResult(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java index 6d3c4c4..4faa1b3 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/ListProfilesTest.java @@ -26,8 +26,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listGenerateKeyProfiles(SOP sop) { - List profiles = sop - .listProfiles() + List profiles = assumeSupported(sop::listProfiles) .generateKey(); assertFalse(profiles.isEmpty()); @@ -36,8 +35,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listEncryptProfiles(SOP sop) { - List profiles = sop - .listProfiles() + List profiles = assumeSupported(sop::listProfiles) .encrypt(); assertFalse(profiles.isEmpty()); @@ -46,8 +44,7 @@ public class ListProfilesTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void listUnsupportedProfiles(SOP sop) { - assertThrows(SOPGPException.UnsupportedProfile.class, () -> sop - .listProfiles() + assertThrows(SOPGPException.UnsupportedProfile.class, () -> assumeSupported(sop::listProfiles) .subcommand("invalid")); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java index b577017..501f53c 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/MergeCertsTest.java @@ -24,16 +24,16 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testMergeWithItself(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(cert) .getBytes(); @@ -44,17 +44,17 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testMergeWithItselfArmored(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .noArmor() .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(cert) .getBytes(); @@ -65,18 +65,18 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testMergeWithItselfViaBase(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); byte[] certs = ArraysKt.plus(cert, cert); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(certs) .getBytes(); @@ -87,20 +87,20 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testApplyBaseToUpdate(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] update = sop.revokeKey() + byte[] update = assumeSupported(sop::revokeKey) .keys(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(cert) .baseCertificates(update) .getBytes(); @@ -111,20 +111,20 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testApplyUpdateToBase(SOP sop) throws IOException { - byte[] key = sop.generateKey() + byte[] key = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] cert = sop.extractCert() + byte[] cert = assumeSupported(sop::extractCert) .key(key) .getBytes(); - byte[] update = sop.revokeKey() + byte[] update = assumeSupported(sop::revokeKey) .keys(key) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(update) .baseCertificates(cert) .getBytes(); @@ -135,25 +135,25 @@ public class MergeCertsTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void testApplyUpdateToMissingBaseDoesNothing(SOP sop) throws IOException { - byte[] aliceKey = sop.generateKey() + byte[] aliceKey = assumeSupported(sop::generateKey) .userId("Alice ") .generate() .getBytes(); - byte[] aliceCert = sop.extractCert() + byte[] aliceCert = assumeSupported(sop::extractCert) .key(aliceKey) .getBytes(); - byte[] bobKey = sop.generateKey() + byte[] bobKey = assumeSupported(sop::generateKey) .userId("Bob ") .generate() .getBytes(); - byte[] bobCert = sop.extractCert() + byte[] bobCert = assumeSupported(sop::extractCert) .key(bobKey) .getBytes(); - byte[] merged = sop.mergeCerts() + byte[] merged = assumeSupported(sop::mergeCerts) .updates(bobCert) .baseCertificates(aliceCert) .getBytes(); diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java index cb51332..1880d58 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/RevokeKeyTest.java @@ -36,8 +36,8 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKey(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).keys(secretKey).getBytes(); assertTrue(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); assertFalse(Arrays.equals(secretKey, revocation)); @@ -46,8 +46,8 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKeyNoArmor(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).noArmor().keys(secretKey).getBytes(); assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); } @@ -55,8 +55,8 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeUnprotectedKeyUnarmored(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().userId("Alice ").noArmor().generate().getBytes(); - byte[] revocation = sop.revokeKey().noArmor().keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).userId("Alice ").noArmor().generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).noArmor().keys(secretKey).getBytes(); assertFalse(JUtils.arrayStartsWith(revocation, TestData.BEGIN_PGP_PUBLIC_KEY_BLOCK)); assertFalse(Arrays.equals(secretKey, revocation)); @@ -65,18 +65,18 @@ public class RevokeKeyTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void revokeCertificateFails(SOP sop) throws IOException { - byte[] secretKey = sop.generateKey().generate().getBytes(); - byte[] certificate = sop.extractCert().key(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] certificate = assumeSupported(sop::extractCert).key(secretKey).getBytes(); - assertThrows(SOPGPException.BadData.class, () -> sop.revokeKey().keys(certificate).getBytes()); + assertThrows(SOPGPException.BadData.class, () -> assumeSupported(sop::revokeKey).keys(certificate).getBytes()); } @ParameterizedTest @MethodSource("provideInstances") public void revokeProtectedKey(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().withKeyPassword(password).keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(password).keys(secretKey).getBytes(); assertFalse(Arrays.equals(secretKey, revocation)); } @@ -86,8 +86,8 @@ public class RevokeKeyTest extends AbstractSOPTest { public void revokeProtectedKeyWithMultiplePasswordOptions(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); byte[] wrongPassword = "0r4ng3".getBytes(UTF8Util.UTF8); - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); - byte[] revocation = sop.revokeKey().withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] revocation = assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).withKeyPassword(password).keys(secretKey).getBytes(); assertFalse(Arrays.equals(secretKey, revocation)); } @@ -96,9 +96,9 @@ public class RevokeKeyTest extends AbstractSOPTest { @MethodSource("provideInstances") public void revokeProtectedKeyWithMissingPassphraseFails(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().keys(secretKey).getBytes()); + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).keys(secretKey).getBytes()); } @ParameterizedTest @@ -106,27 +106,27 @@ public class RevokeKeyTest extends AbstractSOPTest { public void revokeProtectedKeyWithWrongPassphraseFails(SOP sop) throws IOException { byte[] password = "sw0rdf1sh".getBytes(UTF8Util.UTF8); String wrongPassword = "or4ng3"; - byte[] secretKey = sop.generateKey().withKeyPassword(password).userId("Alice ").generate().getBytes(); + byte[] secretKey = assumeSupported(sop::generateKey).withKeyPassword(password).userId("Alice ").generate().getBytes(); - assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().withKeyPassword(wrongPassword).keys(secretKey).getBytes()); + assertThrows(SOPGPException.KeyIsProtected.class, () -> assumeSupported(sop::revokeKey).withKeyPassword(wrongPassword).keys(secretKey).getBytes()); } @ParameterizedTest @MethodSource("provideInstances") public void revokeKeyIsNowHardRevoked(SOP sop) throws IOException { - byte[] key = sop.generateKey().generate().getBytes(); - byte[] cert = sop.extractCert().key(key).getBytes(); + byte[] key = assumeSupported(sop::generateKey).generate().getBytes(); + byte[] cert = assumeSupported(sop::extractCert).key(key).getBytes(); // Sign a message with the key byte[] msg = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); - byte[] signedMsg = sop.inlineSign().key(key).data(msg).getBytes(); + byte[] signedMsg = assumeSupported(sop::inlineSign).key(key).data(msg).getBytes(); // Verifying the message with the valid cert works - List result = sop.inlineVerify().cert(cert).data(signedMsg).toByteArrayAndResult().getResult(); + List result = assumeSupported(sop::inlineVerify).cert(cert).data(signedMsg).toByteArrayAndResult().getResult(); VerificationListAssert.assertThatVerificationList(result).hasSingleItem(); // Now hard revoke the key and re-check signature, expecting no valid certification - byte[] revokedCert = sop.revokeKey().keys(key).getBytes(); - assertThrows(SOPGPException.NoSignature.class, () -> sop.inlineVerify().cert(revokedCert).data(signedMsg).toByteArrayAndResult()); + byte[] revokedCert = assumeSupported(sop::revokeKey).keys(key).getBytes(); + assertThrows(SOPGPException.NoSignature.class, () -> assumeSupported(sop::inlineVerify).cert(revokedCert).data(signedMsg).toByteArrayAndResult()); } } diff --git a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java index 47644bf..71f7efd 100644 --- a/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java +++ b/sop-java-testfixtures/src/main/java/sop/testsuite/operation/VersionTest.java @@ -28,7 +28,7 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void versionNameTest(SOP sop) { - String name = sop.version().getName(); + String name = assumeSupported(sop::version).getName(); assertNotNull(name); assertFalse(name.isEmpty()); } @@ -36,21 +36,21 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void versionVersionTest(SOP sop) { - String version = sop.version().getVersion(); + String version = assumeSupported(sop::version).getVersion(); assertFalse(version.isEmpty()); } @ParameterizedTest @MethodSource("provideInstances") public void backendVersionTest(SOP sop) { - String backend = sop.version().getBackendVersion(); + String backend = assumeSupported(sop::version).getBackendVersion(); assertFalse(backend.isEmpty()); } @ParameterizedTest @MethodSource("provideInstances") public void extendedVersionTest(SOP sop) { - String extended = sop.version().getExtendedVersion(); + String extended = assumeSupported(sop::version).getExtendedVersion(); assertFalse(extended.isEmpty()); } @@ -58,27 +58,27 @@ public class VersionTest extends AbstractSOPTest { @MethodSource("provideInstances") public void sopSpecVersionTest(SOP sop) { try { - sop.version().getSopSpecVersion(); + assumeSupported(sop::version).getSopSpecVersion(); } catch (RuntimeException e) { throw new TestAbortedException("SOP backend does not support 'version --sop-spec' yet."); } - String sopSpec = sop.version().getSopSpecVersion(); - if (sop.version().isSopSpecImplementationIncomplete()) { + String sopSpec = assumeSupported(sop::version).getSopSpecVersion(); + if (assumeSupported(sop::version).isSopSpecImplementationIncomplete()) { assertTrue(sopSpec.startsWith("~draft-dkg-openpgp-stateless-cli-")); } else { assertTrue(sopSpec.startsWith("draft-dkg-openpgp-stateless-cli-")); } - int sopRevision = sop.version().getSopSpecRevisionNumber(); - assertTrue(sop.version().getSopSpecRevisionName().endsWith("" + sopRevision)); + int sopRevision = assumeSupported(sop::version).getSopSpecRevisionNumber(); + assertTrue(assumeSupported(sop::version).getSopSpecRevisionName().endsWith("" + sopRevision)); } @ParameterizedTest @MethodSource("provideInstances") public void sopVVersionTest(SOP sop) { try { - sop.version().getSopVVersion(); + assumeSupported(sop::version).getSopVVersion(); } catch (SOPGPException.UnsupportedOption e) { throw new TestAbortedException( "Implementation does (gracefully) not provide coverage for any sopv interface version."); @@ -90,6 +90,6 @@ public class VersionTest extends AbstractSOPTest { @ParameterizedTest @MethodSource("provideInstances") public void sopJavaVersionTest(SOP sop) { - assertNotNull(sop.version().getSopJavaVersion()); + assertNotNull(assumeSupported(sop::version).getSopJavaVersion()); } } diff --git a/sop-java/src/test/java/sop/VerificationJSONTest.java b/sop-java/src/test/java/sop/VerificationJSONTest.java index a80e6fc..a6e5d96 100644 --- a/sop-java/src/test/java/sop/VerificationJSONTest.java +++ b/sop-java/src/test/java/sop/VerificationJSONTest.java @@ -7,6 +7,7 @@ package sop; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Test; import sop.enums.SignatureMode; +import sop.testsuite.assertions.VerificationAssert; import java.text.ParseException; import java.util.ArrayList; @@ -158,6 +159,8 @@ public class VerificationJSONTest { assertNull(json.getExt()); verification = new Verification(verification.getCreationTime(), verification.getSigningKeyFingerprint(), verification.getSigningCertFingerprint(), verification.getSignatureMode().get(), json, dummySerializer); + VerificationAssert.assertThatVerification(verification) + .hasJSON(dummyParser, j -> j.getSigners().contains("alice.pgp")); assertEquals(string, verification.toString()); } } From d32d9b54d75586c834d73b08a6b2c48d7b9d3ddc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 1 Jul 2025 15:06:50 +0200 Subject: [PATCH 234/238] Depend on junit-platform-suite to avoid needing to inherit test suite for external-sop tests --- external-sop/build.gradle | 4 +++- .../sop/testsuite/external/ExternalTestSuite.java | 14 ++++++++++++++ .../operation/ExternalArmorDearmorTest.java | 13 ------------- .../ExternalCertifyValidateUserIdTest.java | 13 ------------- .../operation/ExternalChangeKeyPasswordTest.java | 13 ------------- .../ExternalDecryptWithSessionKeyTest.java | 13 ------------- .../ExternalDetachedSignDetachedVerifyTest.java | 12 ------------ .../operation/ExternalEncryptDecryptTest.java | 13 ------------- .../operation/ExternalExtractCertTest.java | 13 ------------- .../operation/ExternalGenerateKeyTest.java | 13 ------------- ...alInlineSignInlineDetachDetachedVerifyTest.java | 14 -------------- .../ExternalInlineSignInlineVerifyTest.java | 13 ------------- .../operation/ExternalListProfilesTest.java | 13 ------------- .../external/operation/ExternalMergeCertsTest.java | 13 ------------- .../external/operation/ExternalRevokeKeyTest.java | 13 ------------- .../external/operation/ExternalVersionTest.java | 13 ------------- 16 files changed, 17 insertions(+), 183 deletions(-) create mode 100644 external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java delete mode 100644 external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java diff --git a/external-sop/build.gradle b/external-sop/build.gradle index d1a7ffb..2dfbf7e 100644 --- a/external-sop/build.gradle +++ b/external-sop/build.gradle @@ -15,7 +15,9 @@ repositories { dependencies { testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" - testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + testImplementation "org.junit.platform:junit-platform-suite-api:1.13.2" + testRuntimeOnly 'org.junit.platform:junit-platform-suite:1.13.2' api project(":sop-java") api "org.slf4j:slf4j-api:$slf4jVersion" diff --git a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java new file mode 100644 index 0000000..6e991cf --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java @@ -0,0 +1,14 @@ +package sop.testsuite.external; + +import org.junit.platform.suite.api.IncludeClassNamePatterns; +import org.junit.platform.suite.api.SelectPackages; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +@Suite +@SuiteDisplayName("External SOP Tests") +@SelectPackages("sop.testsuite.operation") +@IncludeClassNamePatterns(".*Test") +public class ExternalTestSuite { + +} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java deleted file mode 100644 index 1d8ff2b..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalArmorDearmorTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ArmorDearmorTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalArmorDearmorTest extends ArmorDearmorTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java deleted file mode 100644 index bb319ca..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalCertifyValidateUserIdTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.CertifyValidateUserIdTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalCertifyValidateUserIdTest extends CertifyValidateUserIdTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java deleted file mode 100644 index 42a9693..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalChangeKeyPasswordTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ChangeKeyPasswordTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalChangeKeyPasswordTest extends ChangeKeyPasswordTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java deleted file mode 100644 index 0ac03a4..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDecryptWithSessionKeyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.DecryptWithSessionKeyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalDecryptWithSessionKeyTest extends DecryptWithSessionKeyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java deleted file mode 100644 index 13959df..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalDetachedSignDetachedVerifyTest.java +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.DetachedSignDetachedVerifyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalDetachedSignDetachedVerifyTest extends DetachedSignDetachedVerifyTest { -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java deleted file mode 100644 index b83ca46..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalEncryptDecryptTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.EncryptDecryptTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalEncryptDecryptTest extends EncryptDecryptTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java deleted file mode 100644 index f47656c..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalExtractCertTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ExtractCertTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalExtractCertTest extends ExtractCertTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java deleted file mode 100644 index 7ac971b..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalGenerateKeyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.GenerateKeyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalGenerateKeyTest extends GenerateKeyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java deleted file mode 100644 index 2dd3396..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineDetachDetachedVerifyTest.java +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.InlineSignInlineDetachDetachedVerifyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalInlineSignInlineDetachDetachedVerifyTest - extends InlineSignInlineDetachDetachedVerifyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java deleted file mode 100644 index 24e30aa..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalInlineSignInlineVerifyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.InlineSignInlineVerifyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalInlineSignInlineVerifyTest extends InlineSignInlineVerifyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java deleted file mode 100644 index 18da883..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalListProfilesTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.ListProfilesTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalListProfilesTest extends ListProfilesTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java deleted file mode 100644 index 8b22b37..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalMergeCertsTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.MergeCertsTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalMergeCertsTest extends MergeCertsTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java deleted file mode 100644 index e2efe03..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.RevokeKeyTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalRevokeKeyTest extends RevokeKeyTest { - -} diff --git a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java deleted file mode 100644 index ee63f09..0000000 --- a/external-sop/src/test/java/sop/testsuite/external/operation/ExternalVersionTest.java +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Paul Schaub -// -// SPDX-License-Identifier: Apache-2.0 - -package sop.testsuite.external.operation; - -import org.junit.jupiter.api.condition.EnabledIf; -import sop.testsuite.operation.VersionTest; - -@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") -public class ExternalVersionTest extends VersionTest { - -} From d4e8c14b08ccb5d36c1ebc98bfa06d7f80061dd0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 24 Jul 2025 12:53:27 +0200 Subject: [PATCH 235/238] Update documentation, add withKeyPassphrase(CharArray) methods --- .../main/kotlin/sop/operation/AbstractSign.kt | 13 ++- .../src/main/kotlin/sop/operation/Armor.kt | 1 + .../kotlin/sop/operation/CertifyUserId.kt | 102 +++++++++++++++--- .../kotlin/sop/operation/ChangeKeyPassword.kt | 51 ++++++--- .../src/main/kotlin/sop/operation/Dearmor.kt | 1 + .../src/main/kotlin/sop/operation/Decrypt.kt | 1 + .../main/kotlin/sop/operation/DetachedSign.kt | 1 + .../kotlin/sop/operation/DetachedVerify.kt | 1 + .../src/main/kotlin/sop/operation/Encrypt.kt | 1 + .../main/kotlin/sop/operation/ExtractCert.kt | 1 + .../main/kotlin/sop/operation/GenerateKey.kt | 1 + .../main/kotlin/sop/operation/InlineDetach.kt | 1 + .../main/kotlin/sop/operation/InlineSign.kt | 1 + .../main/kotlin/sop/operation/InlineVerify.kt | 2 +- .../main/kotlin/sop/operation/ListProfiles.kt | 2 +- .../main/kotlin/sop/operation/MergeCerts.kt | 53 +++++++-- .../main/kotlin/sop/operation/RevokeKey.kt | 38 ++++++- .../main/kotlin/sop/operation/UpdateKey.kt | 66 ++++++++---- .../kotlin/sop/operation/ValidateUserId.kt | 51 +++++---- .../kotlin/sop/operation/VerifySignatures.kt | 1 + .../src/main/kotlin/sop/operation/Version.kt | 1 + 21 files changed, 308 insertions(+), 82 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt index 0258432..72b8f72 100644 --- a/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt +++ b/sop-java/src/main/kotlin/sop/operation/AbstractSign.kt @@ -61,9 +61,18 @@ interface AbstractSign { * @param password password * @return builder instance * @throws UnsupportedOption if key passwords are not supported - * @throws PasswordNotHumanReadable if the provided passphrase is not human-readable */ - @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): T = withKeyPassword(password.concatToString()) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): T = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) /** diff --git a/sop-java/src/main/kotlin/sop/operation/Armor.kt b/sop-java/src/main/kotlin/sop/operation/Armor.kt index be7f1a3..b54aed7 100644 --- a/sop-java/src/main/kotlin/sop/operation/Armor.kt +++ b/sop-java/src/main/kotlin/sop/operation/Armor.kt @@ -9,6 +9,7 @@ import java.io.InputStream import sop.Ready import sop.exception.SOPGPException.BadData +/** Interface for armoring binary OpenPGP data. */ interface Armor { /** diff --git a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt index 642966b..d59f9f0 100644 --- a/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/CertifyUserId.kt @@ -7,35 +7,111 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for issuing certifications over UserIDs on certificates. */ interface CertifyUserId { - @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): CertifyUserId + /** Disable ASCII armor for the output. */ + @Throws(UnsupportedOption::class) fun noArmor(): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) fun userId(userId: String): CertifyUserId + /** + * Add a user-id that shall be certified on the certificates. + * + * @param userId user-id + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun userId(userId: String): CertifyUserId - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): CertifyUserId = + withKeyPassword(password.concatToString()) + + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): CertifyUserId = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + /** + * Provide the password for the secret key used for signing. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if key passwords are not supported + * @throws PasswordNotHumanReadable if the provided password is not human-readable + */ + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) fun withKeyPassword(password: ByteArray): CertifyUserId - @Throws(SOPGPException.UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId + /** + * If this option is provided, it is possible to certify user-ids on certificates, which do not + * have a self-certification for the user-id. You can use this option to add pet-name + * certifications to certificates, e.g. "Mom". + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun noRequireSelfSig(): CertifyUserId - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) - fun keys(keys: InputStream): CertifyUserId + /** + * Provide signing keys for issuing the certifications. + * + * @param keys input stream containing one or more signing key + * @return builder instance + * @throws BadData if the keys cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun keys(keys: InputStream): CertifyUserId - @Throws(SOPGPException.BadData::class, IOException::class, SOPGPException.KeyIsProtected::class) + /** + * Provide signing keys for issuing the certifications. + * + * @param keys byte array containing one or more signing key + * @return builder instance + * @throws BadData if the keys cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun keys(keys: ByteArray): CertifyUserId = keys(keys.inputStream()) - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + /** + * Provide the certificates that you want to create certifications for. + * + * @param certs input stream containing the certificates + * @return object to require the certified certificates from + * @throws BadData if the certificates cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if one or more signing keys are passphrase protected and cannot be + * unlocked + */ + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class, KeyIsProtected::class) fun certs(certs: InputStream): Ready - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + /** + * Provide the certificates that you want to create certifications for. + * + * @param certs byte array containing the certificates + * @return object to require the certified certificates from + * @throws BadData if the certificates cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if one or more signing keys are passphrase protected and cannot be + * unlocked + */ + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class, KeyIsProtected::class) fun certs(certs: ByteArray): Ready = certs(certs.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt index 224e0f4..fe9b8c9 100644 --- a/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt +++ b/sop-java/src/main/kotlin/sop/operation/ChangeKeyPassword.kt @@ -11,6 +11,7 @@ import sop.exception.SOPGPException.KeyIsProtected import sop.exception.SOPGPException.PasswordNotHumanReadable import sop.util.UTF8Util +/** Interface for changing key passwords. */ interface ChangeKeyPassword { /** @@ -28,13 +29,8 @@ interface ChangeKeyPassword { * @param oldPassphrase old passphrase * @return builder instance */ - @Throws(PasswordNotHumanReadable::class) - fun oldKeyPassphrase(oldPassphrase: ByteArray): ChangeKeyPassword = - try { - oldKeyPassphrase(UTF8Util.decodeUTF8(oldPassphrase)) - } catch (e: CharacterCodingException) { - throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") - } + fun oldKeyPassphrase(oldPassphrase: CharArray): ChangeKeyPassword = + oldKeyPassphrase(oldPassphrase.concatToString()) /** * Provide a passphrase to unlock the secret key. This method can be provided multiple times to @@ -47,21 +43,33 @@ interface ChangeKeyPassword { fun oldKeyPassphrase(oldPassphrase: String): ChangeKeyPassword /** - * Provide a passphrase to re-lock the secret key with. This method can only be used once, and - * all key material encountered will be encrypted with the given passphrase. If this method is - * not called, the key material will not be protected. + * Provide a passphrase to unlock the secret key. This method can be provided multiple times to + * provide separate passphrases that are tried as a means to unlock any secret key material + * encountered. * - * @param newPassphrase new passphrase + * @param oldPassphrase old passphrase * @return builder instance + * @throws PasswordNotHumanReadable if the old key passphrase is not human-readable */ @Throws(PasswordNotHumanReadable::class) - fun newKeyPassphrase(newPassphrase: ByteArray): ChangeKeyPassword = + fun oldKeyPassphrase(oldPassphrase: ByteArray): ChangeKeyPassword = try { - newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)) + oldKeyPassphrase(UTF8Util.decodeUTF8(oldPassphrase)) } catch (e: CharacterCodingException) { throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") } + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + */ + fun newKeyPassphrase(newPassphrase: CharArray): ChangeKeyPassword = + newKeyPassphrase(newPassphrase.concatToString()) + /** * Provide a passphrase to re-lock the secret key with. This method can only be used once, and * all key material encountered will be encrypted with the given passphrase. If this method is @@ -72,6 +80,23 @@ interface ChangeKeyPassword { */ fun newKeyPassphrase(newPassphrase: String): ChangeKeyPassword + /** + * Provide a passphrase to re-lock the secret key with. This method can only be used once, and + * all key material encountered will be encrypted with the given passphrase. If this method is + * not called, the key material will not be protected. + * + * @param newPassphrase new passphrase + * @return builder instance + * @throws PasswordNotHumanReadable if the passphrase is not human-readable + */ + @Throws(PasswordNotHumanReadable::class) + fun newKeyPassphrase(newPassphrase: ByteArray): ChangeKeyPassword = + try { + newKeyPassphrase(UTF8Util.decodeUTF8(newPassphrase)) + } catch (e: CharacterCodingException) { + throw PasswordNotHumanReadable("Password MUST be a valid UTF8 string.") + } + /** * Provide the key material. * diff --git a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt index cc5e98d..2984f27 100644 --- a/sop-java/src/main/kotlin/sop/operation/Dearmor.kt +++ b/sop-java/src/main/kotlin/sop/operation/Dearmor.kt @@ -10,6 +10,7 @@ import sop.Ready import sop.exception.SOPGPException.BadData import sop.util.UTF8Util +/** Interface for removing ASCII armor from OpenPGP data. */ interface Dearmor { /** diff --git a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt index ae228e9..4d009f9 100644 --- a/sop-java/src/main/kotlin/sop/operation/Decrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Decrypt.kt @@ -13,6 +13,7 @@ import sop.SessionKey import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for decrypting encrypted OpenPGP messages. */ interface Decrypt { /** diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt index c0e62dd..4aaadc1 100644 --- a/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt +++ b/sop-java/src/main/kotlin/sop/operation/DetachedSign.kt @@ -11,6 +11,7 @@ import sop.SigningResult import sop.enums.SignAs import sop.exception.SOPGPException.* +/** Interface for creating detached signatures over plaintext messages. */ interface DetachedSign : AbstractSign { /** diff --git a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt index d899b54..319658d 100644 --- a/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt +++ b/sop-java/src/main/kotlin/sop/operation/DetachedVerify.kt @@ -8,6 +8,7 @@ import java.io.IOException import java.io.InputStream import sop.exception.SOPGPException.BadData +/** Interface for verifying detached OpenPGP signatures over plaintext messages. */ interface DetachedVerify : AbstractVerify, VerifySignatures { /** diff --git a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt index 71c04cb..02c7f97 100644 --- a/sop-java/src/main/kotlin/sop/operation/Encrypt.kt +++ b/sop-java/src/main/kotlin/sop/operation/Encrypt.kt @@ -13,6 +13,7 @@ import sop.enums.EncryptAs import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for creating encrypted OpenPGP messages. */ interface Encrypt { /** diff --git a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt index e2ce1cc..6485bc2 100644 --- a/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt +++ b/sop-java/src/main/kotlin/sop/operation/ExtractCert.kt @@ -9,6 +9,7 @@ import java.io.InputStream import sop.Ready import sop.exception.SOPGPException.BadData +/** Interface for extracting certificates from OpenPGP keys. */ interface ExtractCert { /** diff --git a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt index 13de39a..bccd372 100644 --- a/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/GenerateKey.kt @@ -10,6 +10,7 @@ import sop.Ready import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for generating OpenPGP keys. */ interface GenerateKey { /** diff --git a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt index 941a9bf..1cc64ce 100644 --- a/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt +++ b/sop-java/src/main/kotlin/sop/operation/InlineDetach.kt @@ -10,6 +10,7 @@ import sop.ReadyWithResult import sop.Signatures import sop.exception.SOPGPException.BadData +/** Interface for detaching inline signatures from OpenPGP messages. */ interface InlineDetach { /** diff --git a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt index 11b5668..6855a61 100644 --- a/sop-java/src/main/kotlin/sop/operation/InlineSign.kt +++ b/sop-java/src/main/kotlin/sop/operation/InlineSign.kt @@ -10,6 +10,7 @@ import sop.Ready import sop.enums.InlineSignAs import sop.exception.SOPGPException.* +/** Interface for creating inline-signed OpenPGP messages. */ interface InlineSign : AbstractSign { /** diff --git a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt index c16b269..a944957 100644 --- a/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt +++ b/sop-java/src/main/kotlin/sop/operation/InlineVerify.kt @@ -11,7 +11,7 @@ import sop.Verification import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.NoSignature -/** API for verification of inline-signed messages. */ +/** Interface for verification of inline-signed messages. */ interface InlineVerify : AbstractVerify { /** diff --git a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt index 315faf2..0bed1f8 100644 --- a/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt +++ b/sop-java/src/main/kotlin/sop/operation/ListProfiles.kt @@ -6,7 +6,7 @@ package sop.operation import sop.Profile -/** Subcommand to list supported profiles of other subcommands. */ +/** Interface to list supported profiles of other subcommands. */ interface ListProfiles { /** diff --git a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt index f922490..20469cb 100644 --- a/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt +++ b/sop-java/src/main/kotlin/sop/operation/MergeCerts.kt @@ -7,21 +7,58 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* +/** Interface for merging multiple copies of the same certificate into one. */ interface MergeCerts { - @Throws(SOPGPException.UnsupportedOption::class) fun noArmor(): MergeCerts + /** + * Disable ASCII armor for the output certificate. + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun noArmor(): MergeCerts - @Throws(SOPGPException.BadData::class, IOException::class) - fun updates(updateCerts: InputStream): MergeCerts + /** + * Provide updated copies of the base certificate. + * + * @param updateCerts input stream containing an updated copy of the base cert + * @return builder instance + * @throws BadData if the update cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun updates(updateCerts: InputStream): MergeCerts - @Throws(SOPGPException.BadData::class, IOException::class) + /** + * Provide updated copies of the base certificate. + * + * @param updateCerts byte array containing an updated copy of the base cert + * @return builder instance + * @throws BadData if the update cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun updates(updateCerts: ByteArray): MergeCerts = updates(updateCerts.inputStream()) - @Throws(SOPGPException.BadData::class, IOException::class) - fun baseCertificates(certs: InputStream): Ready + /** + * Provide the base certificate into which updates shall be merged. + * + * @param certs input stream containing the base OpenPGP certificate + * @return object to require the merged certificate from + * @throws BadData if the base certificate cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun baseCertificates(certs: InputStream): Ready - @Throws(SOPGPException.BadData::class, IOException::class) + /** + * Provide the base certificate into which updates shall be merged. + * + * @param certs byte array containing the base OpenPGP certificate + * @return object to require the merged certificate from + * @throws BadData if the base certificate cannot be read + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, IOException::class) fun baseCertificates(certs: ByteArray): Ready = baseCertificates(certs.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt index f3cbe5c..13c6712 100644 --- a/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/RevokeKey.kt @@ -4,12 +4,13 @@ package sop.operation +import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException.PasswordNotHumanReadable -import sop.exception.SOPGPException.UnsupportedOption +import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for creating certificate revocations. */ interface RevokeKey { /** @@ -25,9 +26,18 @@ interface RevokeKey { * @param password password * @return builder instance * @throws UnsupportedOption if the implementation does not support key passwords - * @throws PasswordNotHumanReadable if the password is not human-readable */ - @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): RevokeKey = withKeyPassword(password.concatToString()) + + /** + * Provide the decryption password for the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if the implementation does not support key passwords + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): RevokeKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @@ -42,7 +52,27 @@ interface RevokeKey { @Throws(UnsupportedOption::class, PasswordNotHumanReadable::class) fun withKeyPassword(password: ByteArray): RevokeKey + /** + * Provide the key that you want to revoke. + * + * @param bytes byte array containing the OpenPGP key + * @return object to require the revocation certificate from + * @throws BadData if the key cannot be read + * @throws KeyIsProtected if the key is protected and cannot be unlocked + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, KeyIsProtected::class, IOException::class) fun keys(bytes: ByteArray): Ready = keys(bytes.inputStream()) + /** + * Provide the key that you want to revoke. + * + * @param keys input stream containing the OpenPGP key + * @return object to require the revocation certificate from + * @throws BadData if the key cannot be read + * @throws KeyIsProtected if the key is protected and cannot be unlocked + * @throws IOException if an IO error occurs + */ + @Throws(BadData::class, KeyIsProtected::class, IOException::class) fun keys(keys: InputStream): Ready } diff --git a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt index 1226ed5..13a4bdc 100644 --- a/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt +++ b/sop-java/src/main/kotlin/sop/operation/UpdateKey.kt @@ -7,9 +7,10 @@ package sop.operation import java.io.IOException import java.io.InputStream import sop.Ready -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* import sop.util.UTF8Util +/** Interface for bringing an OpenPGP key up to date. */ interface UpdateKey { /** @@ -22,21 +23,39 @@ interface UpdateKey { /** * Allow key to be used for signing only. If this option is not present, the operation may add a * new, encryption-capable component key. + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.UnsupportedOption::class) fun signingOnly(): UpdateKey + @Throws(UnsupportedOption::class) fun signingOnly(): UpdateKey /** * Do not allow adding new capabilities to the key. If this option is not present, the operation * may add support for new capabilities to the key. + * + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey + @Throws(UnsupportedOption::class) fun noAddedCapabilities(): UpdateKey /** * Provide a passphrase for unlocking the secret key. * * @param password password + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + @Throws(UnsupportedOption::class) + fun withKeyPassword(password: CharArray): UpdateKey = withKeyPassword(password.concatToString()) + + /** + * Provide a passphrase for unlocking the secret key. + * + * @param password password + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun withKeyPassword(password: String): UpdateKey = withKeyPassword(password.toByteArray(UTF8Util.UTF8)) @@ -44,8 +63,11 @@ interface UpdateKey { * Provide a passphrase for unlocking the secret key. * * @param password password + * @return builder instance + * @throws PasswordNotHumanReadable if the password is not human-readable + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.PasswordNotHumanReadable::class, SOPGPException.UnsupportedOption::class) + @Throws(PasswordNotHumanReadable::class, UnsupportedOption::class) fun withKeyPassword(password: ByteArray): UpdateKey /** @@ -53,9 +75,12 @@ interface UpdateKey { * These certificates will be merged into the key. * * @param certs input stream of certificates + * @return builder instance + * @throws UnsupportedOption if this option is not supported + * @throws BadData if the certificate cannot be read + * @throws IOException if an IO error occurs */ - @Throws( - SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws(UnsupportedOption::class, BadData::class, IOException::class) fun mergeCerts(certs: InputStream): UpdateKey /** @@ -63,9 +88,12 @@ interface UpdateKey { * These certificates will be merged into the key. * * @param certs binary certificates + * @return builder instance + * @throws UnsupportedOption if this option is not supported + * @throws BadData if the certificate cannot be read + * @throws IOException if an IO error occurs */ - @Throws( - SOPGPException.UnsupportedOption::class, SOPGPException.BadData::class, IOException::class) + @Throws(UnsupportedOption::class, BadData::class, IOException::class) fun mergeCerts(certs: ByteArray): UpdateKey = mergeCerts(certs.inputStream()) /** @@ -73,12 +101,12 @@ interface UpdateKey { * * @param key input stream containing the key * @return handle to acquire the updated OpenPGP key from + * @throws BadData if the key cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if the key is passphrase protected and cannot be unlocked + * @throws PrimaryKeyBad if the primary key is bad (e.g. expired, too weak) */ - @Throws( - SOPGPException.BadData::class, - IOException::class, - SOPGPException.KeyIsProtected::class, - SOPGPException.PrimaryKeyBad::class) + @Throws(BadData::class, IOException::class, KeyIsProtected::class, PrimaryKeyBad::class) fun key(key: InputStream): Ready /** @@ -86,11 +114,11 @@ interface UpdateKey { * * @param key binary OpenPGP key * @return handle to acquire the updated OpenPGP key from + * @throws BadData if the key cannot be read + * @throws IOException if an IO error occurs + * @throws KeyIsProtected if the key is passphrase protected and cannot be unlocked + * @throws PrimaryKeyBad if the primary key is bad (e.g. expired, too weak) */ - @Throws( - SOPGPException.BadData::class, - IOException::class, - SOPGPException.KeyIsProtected::class, - SOPGPException.PrimaryKeyBad::class) + @Throws(BadData::class, IOException::class, KeyIsProtected::class, PrimaryKeyBad::class) fun key(key: ByteArray): Ready = key(key.inputStream()) } diff --git a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt index fe20fd4..971de25 100644 --- a/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt +++ b/sop-java/src/main/kotlin/sop/operation/ValidateUserId.kt @@ -7,9 +7,9 @@ package sop.operation import java.io.IOException import java.io.InputStream import java.util.* -import sop.exception.SOPGPException +import sop.exception.SOPGPException.* -/** Subcommand to validate UserIDs on certificates. */ +/** Interface to validate UserIDs on certificates. */ interface ValidateUserId { /** @@ -17,15 +17,16 @@ interface ValidateUserId { * e-mail address part of each correctly bound User ID. The rest of each correctly bound User ID * is ignored. * - * @return this + * @return builder instance + * @throws UnsupportedOption if this option is not supported */ - @Throws(SOPGPException.UnsupportedOption::class) fun addrSpecOnly(): ValidateUserId + @Throws(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 + * @return builder instance */ fun userId(userId: String): ValidateUserId @@ -34,19 +35,22 @@ interface ValidateUserId { * if it was bound by an authoritative certificate. * * @param certs authoritative certificates - * @return this + * @return builder instance + * @throws BadData if the authority certificates cannot be read + * @throws IOException if an IO error occurs */ - @Throws(SOPGPException.BadData::class, IOException::class) - fun authorities(certs: InputStream): ValidateUserId + @Throws(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 + * @return builder instance + * @throws BadData if the authority certificates cannot be read + * @throws IOException if an IO error occurs */ - @Throws(SOPGPException.BadData::class, IOException::class) + @Throws(BadData::class, IOException::class) fun authorities(certs: ByteArray): ValidateUserId = authorities(certs.inputStream()) /** @@ -54,13 +58,12 @@ interface ValidateUserId { * * @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 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 CertUserIdNoMatch if any subject certificate does not have a correctly bound UserID + * that matches [userId]. */ - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class) fun subjects(certs: InputStream): Boolean /** @@ -68,14 +71,20 @@ interface ValidateUserId { * * @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 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 CertUserIdNoMatch if any subject certificate does not have a correctly bound UserID + * that matches [userId]. */ - @Throws( - SOPGPException.BadData::class, IOException::class, SOPGPException.CertUserIdNoMatch::class) + @Throws(BadData::class, IOException::class, CertUserIdNoMatch::class) fun subjects(certs: ByteArray): Boolean = subjects(certs.inputStream()) - fun validateAt(date: Date): ValidateUserId + /** + * Provide a reference time for user-id validation. + * + * @param date reference time + * @return builder instance + * @throws UnsupportedOption if this option is not supported + */ + @Throws(UnsupportedOption::class) fun validateAt(date: Date): ValidateUserId } diff --git a/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt index b75e4a5..00a64aa 100644 --- a/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt +++ b/sop-java/src/main/kotlin/sop/operation/VerifySignatures.kt @@ -10,6 +10,7 @@ import sop.Verification import sop.exception.SOPGPException.BadData import sop.exception.SOPGPException.NoSignature +/** API handle for verifying signatures. */ interface VerifySignatures { /** diff --git a/sop-java/src/main/kotlin/sop/operation/Version.kt b/sop-java/src/main/kotlin/sop/operation/Version.kt index 6c8aa95..8a4c808 100644 --- a/sop-java/src/main/kotlin/sop/operation/Version.kt +++ b/sop-java/src/main/kotlin/sop/operation/Version.kt @@ -10,6 +10,7 @@ import java.util.* import kotlin.jvm.Throws import sop.exception.SOPGPException +/** Interface for acquiring version information about the SOP implementation. */ interface Version { /** From e680f3450a392aab2569fd97e6e993ba65fd63af Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Jul 2025 12:21:06 +0200 Subject: [PATCH 236/238] SOPV: Document since when operations are available --- sop-java/src/main/kotlin/sop/SOPV.kt | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/sop-java/src/main/kotlin/sop/SOPV.kt b/sop-java/src/main/kotlin/sop/SOPV.kt index 27eb6e3..d483194 100644 --- a/sop-java/src/main/kotlin/sop/SOPV.kt +++ b/sop-java/src/main/kotlin/sop/SOPV.kt @@ -12,27 +12,41 @@ import sop.operation.Version /** Subset of [SOP] implementing only OpenPGP signature verification. */ interface SOPV { - /** Get information about the implementations name and version. */ + /** + * Get information about the implementations name and version. + * + * @since sopv 1.0 + */ fun version(): Version? /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. + * + * @since sopv 1.0 */ fun verify(): DetachedVerify? = detachedVerify() /** * Verify detached signatures. If you need to verify an inline-signed message, use * [inlineVerify] instead. + * + * @since sopv 1.0 */ fun detachedVerify(): DetachedVerify? /** * Verify signatures of an inline-signed message. If you need to verify detached signatures over * a message, use [detachedVerify] instead. + * + * @since sopv 1.0 */ fun inlineVerify(): InlineVerify? - /** Validate a UserID in an OpenPGP certificate. */ + /** + * Validate a UserID in an OpenPGP certificate. + * + * @since sopv 1.2 + */ fun validateUserId(): ValidateUserId? } From cc08b76b68a66bc8ef094dcaccbee5c561bfabe0 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Jul 2025 14:09:06 +0200 Subject: [PATCH 237/238] Introduce sop-java-json-gson module --- settings.gradle | 3 +- sop-java-json-gson/README.md | 13 +++ sop-java-json-gson/build.gradle | 28 ++++++ .../src/main/kotlin/sop/GsonParser.kt | 23 +++++ .../src/main/kotlin/sop/GsonSerializer.kt | 16 ++++ .../kotlin/sop/GsonSerializerAndParserTest.kt | 96 +++++++++++++++++++ 6 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 sop-java-json-gson/README.md create mode 100644 sop-java-json-gson/build.gradle create mode 100644 sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt create mode 100644 sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt create mode 100644 sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt diff --git a/settings.gradle b/settings.gradle index 1cb66be..84dc381 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,5 +7,6 @@ rootProject.name = 'SOP-Java' include 'sop-java', 'sop-java-picocli', 'sop-java-testfixtures', - 'external-sop' + 'external-sop', + 'sop-java-json-gson' diff --git a/sop-java-json-gson/README.md b/sop-java-json-gson/README.md new file mode 100644 index 0000000..9feb8ff --- /dev/null +++ b/sop-java-json-gson/README.md @@ -0,0 +1,13 @@ + + +# SOP-Java-JSON-GSON + +## JSON Parsing VERIFICATION extension JSON using Gson + +Since revision 11, the SOP specification defines VERIFICATIONS extension JSON. + +This module implements the `JSONParser` and `JSONSerializer` interfaces using Googles Gson library. diff --git a/sop-java-json-gson/build.gradle b/sop-java-json-gson/build.gradle new file mode 100644 index 0000000..4105902 --- /dev/null +++ b/sop-java-json-gson/build.gradle @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + implementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + implementation "org.junit.jupiter:junit-jupiter-params:$junitVersion" + runtimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + implementation project(":sop-java") + api "org.slf4j:slf4j-api:$slf4jVersion" + testImplementation "ch.qos.logback:logback-classic:$logbackVersion" + + // @Nonnull, @Nullable... + implementation "com.google.code.findbugs:jsr305:$jsrVersion" + + api "com.google.code.gson:gson:$gsonVersion" +} diff --git a/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt b/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt new file mode 100644 index 0000000..06adecb --- /dev/null +++ b/sop-java-json-gson/src/main/kotlin/sop/GsonParser.kt @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import com.google.gson.Gson +import com.google.gson.JsonSyntaxException +import com.google.gson.reflect.TypeToken +import java.text.ParseException + +class GsonParser( + private val gson: Gson = Gson() +) : Verification.JSONParser { + + override fun parse(string: String): Verification.JSON { + try { + return gson.fromJson(string, object : TypeToken(){}.type) + } catch (e: JsonSyntaxException) { + throw ParseException(e.message, 0) + } + } +} \ No newline at end of file diff --git a/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt b/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt new file mode 100644 index 0000000..410fe49 --- /dev/null +++ b/sop-java-json-gson/src/main/kotlin/sop/GsonSerializer.kt @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import com.google.gson.Gson + +class GsonSerializer( + private val gson: Gson = Gson() +) : Verification.JSONSerializer { + + override fun serialize(json: Verification.JSON): String { + return gson.toJson(json) + } +} \ No newline at end of file diff --git a/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt b/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt new file mode 100644 index 0000000..9bbef14 --- /dev/null +++ b/sop-java-json-gson/src/test/kotlin/sop/GsonSerializerAndParserTest.kt @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.text.ParseException + +class GsonSerializerAndParserTest { + + private val serializer: GsonSerializer = GsonSerializer() + private val parser: GsonParser = GsonParser() + + @Test + fun simpleSingleTest() { + val before = Verification.JSON("/tmp/alice.pgp") + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun simpleListTest() { + val before = Verification.JSON(listOf("/tmp/alice.pgp", "/tmp/bob.asc")) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\",\"/tmp/bob.asc\"]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withCommentTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + null) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\"}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withExtStringTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + "This is an ext object string.") + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":\"This is an ext object string.\"}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun withExtListTest() { + val before = Verification.JSON( + listOf("/tmp/alice.pgp"), + "This is a comment.", + listOf(1.0,2.0,3.0)) + + val json = serializer.serialize(before) + assertEquals("{\"signers\":[\"/tmp/alice.pgp\"],\"comment\":\"This is a comment.\",\"ext\":[1.0,2.0,3.0]}", json) + + val after = parser.parse(json) + + assertEquals(before, after) + } + + @Test + fun parseInvalidJSON() { + assertThrows { parser.parse("Invalid") } + } + + @Test + fun parseMalformedJSON() { + // Missing '}' + assertThrows { parser.parse("{\"signers\":[\"Alice\"]") } + } +} \ No newline at end of file From b28e234f2124f36a549eab238b1070390ac55dfc Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Fri, 25 Jul 2025 14:09:57 +0200 Subject: [PATCH 238/238] ExternalTestSuite: Add license header --- .../test/java/sop/testsuite/external/ExternalTestSuite.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java index 6e991cf..aa0ae82 100644 --- a/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java +++ b/external-sop/src/test/java/sop/testsuite/external/ExternalTestSuite.java @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2025 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + package sop.testsuite.external; import org.junit.platform.suite.api.IncludeClassNamePatterns;