From 28912618ea0ef0afdd35c89ea61714361e532344 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 5 Jan 2023 03:06:22 +0100 Subject: [PATCH] Initial work on binary-sop This module is intended to allow the use of SOP command line applications such as sqop, pgpainless-sop, etc. as drop-ins for sop-java. --- binary-sop/build.gradle | 24 +++++ .../src/main/java/sop/binary/BinarySop.java | 91 +++++++++++++++++++ .../binary/operation/BinaryExtractCert.java | 69 ++++++++++++++ .../binary/operation/BinaryGenerateKey.java | 89 ++++++++++++++++++ .../sop/binary/operation/BinaryVersion.java | 87 ++++++++++++++++++ .../sop/binary/operation/package-info.java | 8 ++ .../main/java/sop/binary/package-info.java | 8 ++ .../test/java/sop/binary/BinarySopTest.java | 67 ++++++++++++++ settings.gradle | 3 +- 9 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 binary-sop/build.gradle create mode 100644 binary-sop/src/main/java/sop/binary/BinarySop.java create mode 100644 binary-sop/src/main/java/sop/binary/operation/BinaryExtractCert.java create mode 100644 binary-sop/src/main/java/sop/binary/operation/BinaryGenerateKey.java create mode 100644 binary-sop/src/main/java/sop/binary/operation/BinaryVersion.java create mode 100644 binary-sop/src/main/java/sop/binary/operation/package-info.java create mode 100644 binary-sop/src/main/java/sop/binary/package-info.java create mode 100644 binary-sop/src/test/java/sop/binary/BinarySopTest.java diff --git a/binary-sop/build.gradle b/binary-sop/build.gradle new file mode 100644 index 0000000..85b6bc2 --- /dev/null +++ b/binary-sop/build.gradle @@ -0,0 +1,24 @@ +// SPDX-FileCopyrightText: 2021 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +plugins { + id 'java-library' +} + +group 'org.pgpainless' + +repositories { + mavenCentral() +} + +dependencies { + testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" + + api project(":sop-java") +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/binary-sop/src/main/java/sop/binary/BinarySop.java b/binary-sop/src/main/java/sop/binary/BinarySop.java new file mode 100644 index 0000000..c048480 --- /dev/null +++ b/binary-sop/src/main/java/sop/binary/BinarySop.java @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.binary; + +import sop.SOP; +import sop.binary.operation.BinaryExtractCert; +import sop.binary.operation.BinaryGenerateKey; +import sop.binary.operation.BinaryVersion; +import sop.operation.Armor; +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.Version; + +public class BinarySop implements SOP { + + private final String binaryName; + + public BinarySop(String binaryName) { + this.binaryName = binaryName; + } + + @Override + public Version version() { + return new BinaryVersion(binaryName); + } + + @Override + public GenerateKey generateKey() { + return new BinaryGenerateKey(binaryName); + } + + @Override + public ExtractCert extractCert() { + return new BinaryExtractCert(binaryName); + } + + @Override + public DetachedSign detachedSign() { + return null; + } + + @Override + public InlineSign inlineSign() { + return null; + } + + @Override + public DetachedVerify detachedVerify() { + return null; + } + + @Override + public InlineVerify inlineVerify() { + return null; + } + + @Override + public InlineDetach inlineDetach() { + return null; + } + + @Override + public Encrypt encrypt() { + return null; + } + + @Override + public Decrypt decrypt() { + return null; + } + + @Override + public Armor armor() { + return null; + } + + @Override + public Dearmor dearmor() { + return null; + } +} diff --git a/binary-sop/src/main/java/sop/binary/operation/BinaryExtractCert.java b/binary-sop/src/main/java/sop/binary/operation/BinaryExtractCert.java new file mode 100644 index 0000000..7814e2f --- /dev/null +++ b/binary-sop/src/main/java/sop/binary/operation/BinaryExtractCert.java @@ -0,0 +1,69 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.binary.operation; + +import sop.Ready; +import sop.exception.SOPGPException; +import sop.operation.ExtractCert; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class BinaryExtractCert implements ExtractCert { + + private final String binary; + private final Runtime runtime = Runtime.getRuntime(); + + private boolean noArmor; + + public BinaryExtractCert(String binary) { + this.binary = binary; + } + + @Override + public ExtractCert noArmor() { + this.noArmor = true; + return this; + } + + @Override + public Ready key(InputStream keyInputStream) throws IOException, SOPGPException.BadData { + List commandList = new ArrayList<>(); + + commandList.add(binary); + commandList.add("extract-cert"); + + if (noArmor) { + commandList.add("--no-armor"); + } + + String[] command = commandList.toArray(new String[0]); + try { + Process process = runtime.exec(command); + OutputStream stdOut = process.getOutputStream(); + InputStream stdIn = process.getInputStream(); + + return new Ready() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + byte[] buf = new byte[4096]; + int r; + while ((r = keyInputStream.read(buf)) > 0) { + stdOut.write(buf, 0, r); + } + + while ((r = stdIn.read(buf)) > 0) { + outputStream.write(buf, 0 , r); + } + } + }; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/binary-sop/src/main/java/sop/binary/operation/BinaryGenerateKey.java b/binary-sop/src/main/java/sop/binary/operation/BinaryGenerateKey.java new file mode 100644 index 0000000..11ed378 --- /dev/null +++ b/binary-sop/src/main/java/sop/binary/operation/BinaryGenerateKey.java @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.binary.operation; + +import sop.Ready; +import sop.exception.SOPGPException; +import sop.operation.GenerateKey; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +public class BinaryGenerateKey implements GenerateKey { + + private final String binary; + private boolean noArmor = false; + private List userIds = new ArrayList<>(); + private String keyPassword; + + private final Runtime runtime = Runtime.getRuntime(); + + public BinaryGenerateKey(String binary) { + this.binary = binary; + } + + @Override + public GenerateKey noArmor() { + this.noArmor = true; + return this; + } + + @Override + public GenerateKey userId(String userId) { + this.userIds.add(userId); + return this; + } + + @Override + public GenerateKey withKeyPassword(String password) + throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { + this.keyPassword = password; + return this; + } + + @Override + public Ready generate() + throws SOPGPException.MissingArg, SOPGPException.UnsupportedAsymmetricAlgo { + List commandList = new ArrayList<>(); + + commandList.add(binary); + commandList.add("generate-key"); + + if (noArmor) { + commandList.add("--no-armor"); + } + + if (keyPassword != null) { + commandList.add("--with-key-password"); + commandList.add(keyPassword); + } + + for (String userId : userIds) { + commandList.add(userId); + } + + String[] command = commandList.toArray(new String[0]); + try { + Process process = runtime.exec(command); + InputStream stdIn = process.getInputStream(); + + return new Ready() { + @Override + public void writeTo(OutputStream outputStream) throws IOException { + byte[] buf = new byte[4096]; + int r; + while ((r = stdIn.read(buf)) >= 0) { + outputStream.write(buf, 0, r); + } + } + }; + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/binary-sop/src/main/java/sop/binary/operation/BinaryVersion.java b/binary-sop/src/main/java/sop/binary/operation/BinaryVersion.java new file mode 100644 index 0000000..4ca55d4 --- /dev/null +++ b/binary-sop/src/main/java/sop/binary/operation/BinaryVersion.java @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.binary.operation; + +import sop.operation.Version; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +public class BinaryVersion implements Version { + + private final Runtime runtime = Runtime.getRuntime(); + private final String binary; + + public BinaryVersion(String binaryName) { + this.binary = binaryName; + } + + @Override + public String getName() { + String[] command = new String[] {binary, "version"}; + try { + Process process = runtime.exec(command); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line = stdInput.readLine().trim(); + if (line.contains(" ")) { + return line.substring(0, line.lastIndexOf(" ")); + } + return line; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getVersion() { + String[] command = new String[] {binary, "version"}; + try { + Process process = runtime.exec(command); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); + String line = stdInput.readLine().trim(); + if (line.contains(" ")) { + return line.substring(line.lastIndexOf(" ") + 1); + } + return line; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getBackendVersion() { + String[] command = new String[] {binary, "version", "--backend"}; + try { + Process process = runtime.exec(command); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = stdInput.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String getExtendedVersion() { + String[] command = new String[] {binary, "version", "--extended"}; + try { + Process process = runtime.exec(command); + BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder sb = new StringBuilder(); + String line; + while ((line = stdInput.readLine()) != null) { + sb.append(line).append('\n'); + } + return sb.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/binary-sop/src/main/java/sop/binary/operation/package-info.java b/binary-sop/src/main/java/sop/binary/operation/package-info.java new file mode 100644 index 0000000..362fa8a --- /dev/null +++ b/binary-sop/src/main/java/sop/binary/operation/package-info.java @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +/** + * Bindings for SOP subcommands to a SOP binary. + */ +package sop.binary.operation; diff --git a/binary-sop/src/main/java/sop/binary/package-info.java b/binary-sop/src/main/java/sop/binary/package-info.java new file mode 100644 index 0000000..d6c44e1 --- /dev/null +++ b/binary-sop/src/main/java/sop/binary/package-info.java @@ -0,0 +1,8 @@ +// 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.binary; diff --git a/binary-sop/src/test/java/sop/binary/BinarySopTest.java b/binary-sop/src/test/java/sop/binary/BinarySopTest.java new file mode 100644 index 0000000..f095622 --- /dev/null +++ b/binary-sop/src/test/java/sop/binary/BinarySopTest.java @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.binary; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIf; +import sop.SOP; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@EnabledIf("sop.binary.BinarySopTest#sopBinaryInstalled") +public class BinarySopTest { + + private static final String BINARY = "/usr/bin/sqop"; + + private final SOP sop = new BinarySop(BINARY); + + public static boolean sopBinaryInstalled() { + return new File(BINARY).exists(); + } + + @Test + public void versionNameTest() { + assertEquals("sqop", sop.version().getName()); + } + + @Test + public void versionVersionTest() { + String version = sop.version().getVersion(); + assertTrue(version.matches("\\d+(\\.\\d+)*")); + } + + @Test + public void backendVersionTest() { + String backend = sop.version().getBackendVersion(); + assertFalse(backend.isEmpty()); + } + + @Test + public void extendedVersionTest() { + String extended = sop.version().getExtendedVersion(); + assertFalse(extended.isEmpty()); + } + + @Test + public void generateKeyTest() throws IOException { + String key = new String(sop.generateKey().userId("Alice").generate().getBytes()); + assertTrue(key.startsWith("-----BEGIN PGP PRIVATE KEY BLOCK-----\n")); + } + + @Test + @Disabled + public void extractCertTest() throws IOException { + InputStream keyIn = sop.generateKey().userId("Alice").generate().getInputStream(); + String cert = new String(sop.extractCert().key(keyIn).getBytes()); + assertTrue(cert.startsWith("-----BEGIN PGP PUBLIC KEY BLOCK-----\n")); + } +} diff --git a/settings.gradle b/settings.gradle index cc5c0bc..1ac71bb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,5 +5,6 @@ rootProject.name = 'SOP-Java' include 'sop-java', - 'sop-java-picocli' + 'sop-java-picocli', + 'binary-sop'