diff --git a/external-sop/src/main/java/sop/external/ExternalSOP.java b/external-sop/src/main/java/sop/external/ExternalSOP.java index 189b648..c3041ae 100644 --- a/external-sop/src/main/java/sop/external/ExternalSOP.java +++ b/external-sop/src/main/java/sop/external/ExternalSOP.java @@ -19,6 +19,7 @@ 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.Dearmor; @@ -164,7 +165,7 @@ public class ExternalSOP implements SOP { @Override public RevokeKey revokeKey() { - return null; + return new RevokeKeyExternal(binaryName, properties); } @Override diff --git a/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java new file mode 100644 index 0000000..e7aad9d --- /dev/null +++ b/external-sop/src/main/java/sop/external/operation/RevokeKeyExternal.java @@ -0,0 +1,48 @@ +// 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 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 + public RevokeKey noArmor() { + this.commandList.add("--no-armor"); + return this; + } + + @Override + public RevokeKey withKeyPassword(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 + public Ready keys(InputStream keys) { + return ExternalSOP.executeTransformingOperation(Runtime.getRuntime(), commandList, envList, keys); + } +} 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 new file mode 100644 index 0000000..2318abd --- /dev/null +++ b/external-sop/src/test/java/sop/testsuite/external/operation/ExternalRevokeKeyTest.java @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2023 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.testsuite.external.operation; + +import sop.testsuite.operation.RevokeKeyTest; + +public class ExternalRevokeKeyTest extends RevokeKeyTest { + +} diff --git a/sop-java/src/main/java/sop/operation/RevokeKey.java b/sop-java/src/main/java/sop/operation/RevokeKey.java index e2aa433..3ceb5b3 100644 --- a/sop-java/src/main/java/sop/operation/RevokeKey.java +++ b/sop-java/src/main/java/sop/operation/RevokeKey.java @@ -8,6 +8,7 @@ import sop.Ready; import sop.exception.SOPGPException; import sop.util.UTF8Util; +import java.io.ByteArrayInputStream; import java.io.InputStream; public interface RevokeKey { @@ -45,5 +46,9 @@ public interface RevokeKey { 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/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java new file mode 100644 index 0000000..920f877 --- /dev/null +++ b/sop-java/src/testFixtures/java/sop/testsuite/operation/RevokeKeyTest.java @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2023 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 sop.util.UTF8Util; + +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@EnabledIf("sop.testsuite.operation.AbstractSOPTest#hasBackends") +public class RevokeKeyTest extends AbstractSOPTest { + + static Stream provideInstances() { + return provideBackends(); + } + + @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(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @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(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @MethodSource("provideInstances") + 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(); + + assertFalse(Arrays.equals(secretKey, revocation)); + } + + @ParameterizedTest + @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(); + + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().keys(secretKey).getBytes()); + } + + @ParameterizedTest + @MethodSource("provideInstances") + 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(); + + assertThrows(SOPGPException.KeyIsProtected.class, () -> sop.revokeKey().withKeyPassword(wrongPassword).keys(secretKey).getBytes()); + } +}