From 5ee9414410c76b02b20833c10a84f500dca582aa Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Tue, 31 Oct 2023 15:54:54 +0100 Subject: [PATCH] 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()); } }