Merge remote-tracking branch 'origin/sop08'

This commit is contained in:
Paul Schaub 2023-11-15 13:03:07 +01:00
commit 72ca392386
Signed by: vanitasvitae
GPG key ID: 62BEE9264BF17311
8 changed files with 164 additions and 38 deletions

View file

@ -6,6 +6,19 @@ SPDX-License-Identifier: Apache-2.0
# Changelog # 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<EncryptionResult>` 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 ## 7.0.0
- Update implementation to [SOP Specification revision 07](https://www.ietf.org/archive/id/draft-dkg-openpgp-stateless-cli-07.html). - 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 - Add support for new `revoke-key` subcommand

View file

@ -147,7 +147,7 @@ public class ExternalSOP implements SOP {
@Override @Override
public Encrypt encrypt() { public Encrypt encrypt() {
return new EncryptExternal(binaryName, properties); return new EncryptExternal(binaryName, properties, tempDirProvider);
} }
@Override @Override

View file

@ -4,14 +4,19 @@
package sop.external.operation; package sop.external.operation;
import sop.Ready; import sop.EncryptionResult;
import sop.ReadyWithResult;
import sop.SessionKey;
import sop.enums.EncryptAs; import sop.enums.EncryptAs;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.external.ExternalSOP; import sop.external.ExternalSOP;
import sop.operation.Encrypt; import sop.operation.Encrypt;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
@ -21,6 +26,7 @@ import java.util.Properties;
*/ */
public class EncryptExternal implements Encrypt { public class EncryptExternal implements Encrypt {
private final ExternalSOP.TempDirProvider tempDirProvider;
private final List<String> commandList = new ArrayList<>(); private final List<String> commandList = new ArrayList<>();
private final List<String> envList; private final List<String> envList;
private int SIGN_WITH_COUNTER = 0; private int SIGN_WITH_COUNTER = 0;
@ -28,7 +34,8 @@ public class EncryptExternal implements Encrypt {
private int PASSWORD_COUNTER = 0; private int PASSWORD_COUNTER = 0;
private int CERT_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(binary);
this.commandList.add("encrypt"); this.commandList.add("encrypt");
this.envList = ExternalSOP.propertiesToEnv(environment); this.envList = ExternalSOP.propertiesToEnv(environment);
@ -92,8 +99,53 @@ public class EncryptExternal implements Encrypt {
} }
@Override @Override
public Ready plaintext(InputStream plaintext) public ReadyWithResult<EncryptionResult> plaintext(InputStream plaintext)
throws SOPGPException.KeyIsProtected { throws SOPGPException.KeyIsProtected, IOException {
return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, plaintext); 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<EncryptionResult>() {
@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);
}
} }
} }

View file

@ -5,6 +5,7 @@
package sop.cli.picocli.commands package sop.cli.picocli.commands
import java.io.IOException import java.io.IOException
import java.io.PrintWriter
import picocli.CommandLine.* import picocli.CommandLine.*
import sop.cli.picocli.SopCLI import sop.cli.picocli.SopCLI
import sop.enums.EncryptAs import sop.enums.EncryptAs
@ -32,9 +33,14 @@ class EncryptCmd : AbstractSopCmd() {
@Parameters(index = "0..*", paramLabel = "CERTS") var certs: List<String> = listOf() @Parameters(index = "0..*", paramLabel = "CERTS") var certs: List<String> = listOf()
@Option(names = ["--session-key-out"], paramLabel = "SESSIONKEY")
var sessionKeyOut: String? = null
override fun run() { override fun run() {
val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt") val encrypt = throwIfUnsupportedSubcommand(SopCLI.getSop().encrypt(), "encrypt")
throwIfOutputExists(sessionKeyOut)
profile?.let { profile?.let {
try { try {
encrypt.profile(it) encrypt.profile(it)
@ -130,7 +136,22 @@ class EncryptCmd : AbstractSopCmd() {
try { try {
val ready = encrypt.plaintext(System.`in`) 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) { } catch (e: IOException) {
throw RuntimeException(e) throw RuntimeException(e)
} }

View file

@ -4,6 +4,24 @@
package sop.cli.picocli.commands; 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.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never; 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.verify;
import static org.mockito.Mockito.when; 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 { public class EncryptCmdTest {
Encrypt encrypt; Encrypt encrypt;
@ -34,10 +36,10 @@ public class EncryptCmdTest {
@BeforeEach @BeforeEach
public void mockComponents() throws IOException { public void mockComponents() throws IOException {
encrypt = mock(Encrypt.class); encrypt = mock(Encrypt.class);
when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() {
@Override @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 @Test
@ExpectSystemExitWithStatus(1) @ExpectSystemExitWithStatus(1)
public void writeTo_ioExceptionCausesExit1() throws IOException { public void writeTo_ioExceptionCausesExit1() throws IOException {
when(encrypt.plaintext((InputStream) any())).thenReturn(new Ready() { when(encrypt.plaintext((InputStream) any())).thenReturn(new ReadyWithResult<EncryptionResult>() {
@Override @Override
public void writeTo(OutputStream outputStream) throws IOException { public EncryptionResult writeTo(@NotNull OutputStream outputStream) throws IOException, SOPGPException {
throw new IOException(); throw new IOException();
} }
}); });

View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2023 Paul Schaub <vanitasvitae@fsfe.org>
//
// SPDX-License-Identifier: Apache-2.0
package sop
import sop.util.Optional
class EncryptionResult(sessionKey: SessionKey?) {
val sessionKey: Optional<SessionKey>
init {
this.sessionKey = Optional.ofNullable(sessionKey)
}
}

View file

@ -6,8 +6,9 @@ package sop.operation
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
import sop.EncryptionResult
import sop.Profile import sop.Profile
import sop.Ready import sop.ReadyWithResult
import sop.enums.EncryptAs import sop.enums.EncryptAs
import sop.exception.SOPGPException.* import sop.exception.SOPGPException.*
import sop.util.UTF8Util import sop.util.UTF8Util
@ -146,20 +147,22 @@ interface Encrypt {
* Encrypt the given data yielding the ciphertext. * Encrypt the given data yielding the ciphertext.
* *
* @param plaintext plaintext * @param plaintext plaintext
* @return input stream containing the ciphertext * @return result and ciphertext
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws KeyIsProtected if at least one signing key cannot be unlocked * @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<EncryptionResult>
/** /**
* Encrypt the given data yielding the ciphertext. * Encrypt the given data yielding the ciphertext.
* *
* @param plaintext plaintext * @param plaintext plaintext
* @return input stream containing the ciphertext * @return result and ciphertext
* @throws IOException in case of an IO error * @throws IOException in case of an IO error
* @throws KeyIsProtected if at least one signing key cannot be unlocked * @throws KeyIsProtected if at least one signing key cannot be unlocked
*/ */
@Throws(IOException::class, KeyIsProtected::class) @Throws(IOException::class, KeyIsProtected::class)
fun plaintext(plaintext: ByteArray): Ready = plaintext(plaintext.inputStream()) fun plaintext(plaintext: ByteArray): ReadyWithResult<EncryptionResult> =
plaintext(plaintext.inputStream())
} }

View file

@ -10,13 +10,16 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.MethodSource;
import sop.ByteArrayAndResult; import sop.ByteArrayAndResult;
import sop.DecryptionResult; import sop.DecryptionResult;
import sop.EncryptionResult;
import sop.SOP; import sop.SOP;
import sop.SessionKey;
import sop.Verification; import sop.Verification;
import sop.enums.EncryptAs; import sop.enums.EncryptAs;
import sop.enums.SignatureMode; import sop.enums.SignatureMode;
import sop.exception.SOPGPException; import sop.exception.SOPGPException;
import sop.testsuite.TestData; import sop.testsuite.TestData;
import sop.testsuite.assertions.VerificationListAssert; import sop.testsuite.assertions.VerificationListAssert;
import sop.util.Optional;
import sop.util.UTCUtil; import sop.util.UTCUtil;
import java.io.IOException; import java.io.IOException;
@ -27,6 +30,7 @@ import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertArrayEquals; 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.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
@ -41,18 +45,26 @@ public class EncryptDecryptTest extends AbstractSOPTest {
@MethodSource("provideInstances") @MethodSource("provideInstances")
public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException { public void encryptDecryptRoundTripPasswordTest(SOP sop) throws IOException {
byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8); byte[] message = TestData.PLAINTEXT.getBytes(StandardCharsets.UTF_8);
byte[] ciphertext = sop.encrypt() ByteArrayAndResult<EncryptionResult> encResult = sop.encrypt()
.withPassword("sw0rdf1sh") .withPassword("sw0rdf1sh")
.plaintext(message) .plaintext(message)
.getBytes(); .toByteArrayAndResult();
byte[] plaintext = sop.decrypt() byte[] ciphertext = encResult.getBytes();
Optional<SessionKey> encSessionKey = encResult.getResult().getSessionKey();
ByteArrayAndResult<DecryptionResult> decResult = sop.decrypt()
.withPassword("sw0rdf1sh") .withPassword("sw0rdf1sh")
.ciphertext(ciphertext) .ciphertext(ciphertext)
.toByteArrayAndResult() .toByteArrayAndResult();
.getBytes();
byte[] plaintext = decResult.getBytes();
Optional<SessionKey> decSessionKey = decResult.getResult().getSessionKey();
assertArrayEquals(message, plaintext); assertArrayEquals(message, plaintext);
if (encSessionKey.isPresent() && decSessionKey.isPresent()) {
assertEquals(encSessionKey.get(), decSessionKey.get());
}
} }
@ParameterizedTest @ParameterizedTest
@ -62,6 +74,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] ciphertext = sop.encrypt() byte[] ciphertext = sop.encrypt()
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes(); .getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt() ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
@ -83,6 +96,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] ciphertext = sop.encrypt() byte[] ciphertext = sop.encrypt()
.withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8)) .withCert(TestData.BOB_CERT.getBytes(StandardCharsets.UTF_8))
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes(); .getBytes();
byte[] plaintext = sop.decrypt() byte[] plaintext = sop.decrypt()
@ -101,6 +115,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
byte[] ciphertext = sop.encrypt() byte[] ciphertext = sop.encrypt()
.withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8)) .withCert(TestData.CAROL_CERT.getBytes(StandardCharsets.UTF_8))
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes(); .getBytes();
byte[] plaintext = sop.decrypt() byte[] plaintext = sop.decrypt()
@ -120,6 +135,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8)) .withCert(TestData.ALICE_CERT.getBytes(StandardCharsets.UTF_8))
.noArmor() .noArmor()
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes(); .getBytes();
byte[] armored = sop.armor() byte[] armored = sop.armor()
@ -144,6 +160,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(EncryptAs.binary) .mode(EncryptAs.binary)
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes(); .getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt() ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
@ -175,6 +192,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8)) .signWith(TestData.ALICE_KEY.getBytes(StandardCharsets.UTF_8))
.mode(EncryptAs.text) .mode(EncryptAs.text)
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes(); .getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt() ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
@ -215,6 +233,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
.signWith(key) .signWith(key)
.withKeyPassword(keyPassword) .withKeyPassword(keyPassword)
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes(); .getBytes();
ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt() ByteArrayAndResult<DecryptionResult> bytesAndResult = sop.decrypt()
@ -305,6 +324,7 @@ public class EncryptDecryptTest extends AbstractSOPTest {
assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt() assertThrows(SOPGPException.MissingArg.class, () -> sop.encrypt()
.plaintext(message) .plaintext(message)
.toByteArrayAndResult()
.getBytes()); .getBytes());
} }
} }