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 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/kotlin/sop/cli/picocli/commands/EncryptCmd.kt b/sop-java-picocli/src/main/kotlin/sop/cli/picocli/commands/EncryptCmd.kt index b3b0d87..856bc76 100644 --- 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 @@ -5,6 +5,7 @@ package sop.cli.picocli.commands import java.io.IOException +import java.io.PrintWriter import picocli.CommandLine.* import sop.cli.picocli.SopCLI import sop.enums.EncryptAs @@ -32,9 +33,14 @@ class EncryptCmd : AbstractSopCmd() { @Parameters(index = "0..*", paramLabel = "CERTS") var certs: List = listOf() + @Option(names = ["--session-key-out"], paramLabel = "SESSIONKEY") + var sessionKeyOut: String? = null + override fun run() { val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt") + throwIfOutputExists(sessionKeyOut) + profile?.let { try { encrypt.profile(it) @@ -130,7 +136,22 @@ class EncryptCmd : AbstractSopCmd() { try { val ready = encrypt.plaintext(System.`in`) - ready.writeTo(System.out) + val result = ready.writeTo(System.out) + + if (sessionKeyOut == null) { + return + } + + getOutput(sessionKeyOut).use { + if (!result.sessionKey.isPresent) { + val errorMsg = getMsg("sop.error.runtime.no_session_key_extracted") + throw UnsupportedOption(String.format(errorMsg, "--session-key-out")) + } + val sessionKey = result.sessionKey.get() ?: return + val writer = PrintWriter(it) + writer.println(sessionKey) + writer.flush() + } } catch (e: IOException) { throw 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()); } }