From 4e83281213d4f03060eb1f99982148194fa4e2a5 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Thu, 19 Aug 2021 16:44:29 +0200 Subject: [PATCH] Wip: Remove allowNested, add FileUtil --- .../pgpainless/cli/commands/ArmorTest.java | 41 --------- .../java/org/pgpainless/sop/ArmorImpl.java | 27 +----- .../java/org/pgpainless/sop/SignImpl.java | 3 +- .../main/java/sop/cli/picocli/FileUtil.java | 89 +++++++++++++++++++ .../sop/cli/picocli/commands/ArmorCmd.java | 12 --- .../sop/cli/picocli/commands/DecryptCmd.java | 58 ++++++------ .../cli/picocli/commands/ArmorCmdTest.java | 20 ----- .../java/sop/exception/SOPGPException.java | 4 + .../src/main/java/sop/operation/Armor.java | 7 -- 9 files changed, 129 insertions(+), 132 deletions(-) create mode 100644 sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java diff --git a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java index 0e938fa8..c574bfc7 100644 --- a/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java +++ b/pgpainless-cli/src/test/java/org/pgpainless/cli/commands/ArmorTest.java @@ -16,8 +16,6 @@ package org.pgpainless.cli.commands; import static org.junit.jupiter.api.Assertions.assertArrayEquals; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.ByteArrayInputStream; @@ -101,43 +99,4 @@ public class ArmorTest { assertTrue(armored.contains("SGVsbG8sIFdvcmxkIQo=")); } - @Test - @FailOnSystemExit - public void doesNotNestArmorByDefault() { - String armored = "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.69\n" + - "\n" + - "SGVsbG8sIFdvcmxkCg==\n" + - "=fkLo\n" + - "-----END PGP MESSAGE-----"; - - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("armor"); - - assertEquals(armored, out.toString()); - } - - @Test - @FailOnSystemExit - public void testAllowNested() { - String armored = "-----BEGIN PGP MESSAGE-----\n" + - "Version: BCPG v1.69\n" + - "\n" + - "SGVsbG8sIFdvcmxkCg==\n" + - "=fkLo\n" + - "-----END PGP MESSAGE-----"; - - System.setIn(new ByteArrayInputStream(armored.getBytes(StandardCharsets.UTF_8))); - ByteArrayOutputStream out = new ByteArrayOutputStream(); - System.setOut(new PrintStream(out)); - PGPainlessCLI.execute("armor", "--allow-nested"); - - assertNotEquals(armored, out.toString()); - assertTrue(out.toString().contains( - "LS0tLS1CRUdJTiBQR1AgTUVTU0FHRS0tLS0tClZlcnNpb246IEJDUEcgdjEuNjkK\n" + - "ClNHVnNiRzhzSUZkdmNteGtDZz09Cj1ma0xvCi0tLS0tRU5EIFBHUCBNRVNTQUdF\n" + - "LS0tLS0=")); - } } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java index 13bb1e04..62b9baf4 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/ArmorImpl.java @@ -18,9 +18,6 @@ package org.pgpainless.sop; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.PushbackInputStream; -import java.nio.charset.Charset; -import java.util.Arrays; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.util.io.Streams; @@ -32,37 +29,19 @@ import sop.operation.Armor; public class ArmorImpl implements Armor { - public static final byte[] ARMOR_START = "-----BEGIN PGP".getBytes(Charset.forName("UTF8")); - - boolean allowNested = false; - @Override public Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption { throw new SOPGPException.UnsupportedOption("Setting custom Armor labels not supported."); } - @Override - public Armor allowNested() throws SOPGPException.UnsupportedOption { - allowNested = true; - return this; - } - @Override public Ready data(InputStream data) throws SOPGPException.BadData { return new Ready() { @Override public void writeTo(OutputStream outputStream) throws IOException { - PushbackInputStream pbIn = new PushbackInputStream(data, ARMOR_START.length); - byte[] buffer = new byte[ARMOR_START.length]; - int read = pbIn.read(buffer); - pbIn.unread(buffer, 0, read); - if (!allowNested && Arrays.equals(ARMOR_START, buffer)) { - Streams.pipeAll(pbIn, System.out); - } else { - ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(System.out); - Streams.pipeAll(pbIn, armor); - armor.close(); - } + ArmoredOutputStream armor = ArmoredOutputStreamFactory.get(System.out); + Streams.pipeAll(data, armor); + armor.close(); } }; } diff --git a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java index cd6bff94..88268622 100644 --- a/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java +++ b/pgpainless-sop/src/main/java/org/pgpainless/sop/SignImpl.java @@ -45,8 +45,7 @@ public class SignImpl implements Sign { private boolean armor = true; private SignAs mode = SignAs.Binary; - private List keys = new ArrayList<>(); - private SigningOptions signingOptions = new SigningOptions(); + private final SigningOptions signingOptions = new SigningOptions(); @Override public Sign noArmor() { diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java b/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java new file mode 100644 index 00000000..30e2ae0c --- /dev/null +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java @@ -0,0 +1,89 @@ +/* + * Copyright 2021 Paul Schaub. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sop.cli.picocli; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; + +import sop.exception.SOPGPException; + +public class FileUtil { + + private static final String ERROR_AMBIGUOUS = "File name '%s' is ambiguous. File with the same name exists on the filesystem."; + private static final String ERROR_ENV_FOUND = "Environment variable '%s' not set."; + private static final String ERROR_OUTPUT_EXISTS = "Output file '%s' already exists."; + private static final String ERROR_INPUT_NOT_EXIST = "File '%s' does not exist."; + private static final String ERROR_CANNOT_CREATE_FILE = "Output file '%s' cannot be created: %s"; + + public static final String PRFX_ENV = "@ENV:"; + public static final String PRFX_FD = "@FD:"; + + public static File getFile(String fileName) { + if (fileName == null) { + throw new NullPointerException("File name cannot be null."); + } + + if (fileName.startsWith(PRFX_ENV)) { + + if (new File(fileName).exists()) { + throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName)); + } + + String envName = fileName.substring(PRFX_ENV.length()); + String envValue = System.getenv(envName); + if (envValue == null) { + throw new IllegalArgumentException(String.format(ERROR_ENV_FOUND, envName)); + } + return new File(envValue); + } else if (fileName.startsWith(PRFX_FD)) { + + if (new File(fileName).exists()) { + throw new SOPGPException.AmbiguousInput(String.format(ERROR_AMBIGUOUS, fileName)); + } + + throw new IllegalArgumentException("File descriptors not supported."); + } + + return new File(fileName); + } + + public static FileInputStream getFileInputStream(String fileName) { + File file = getFile(fileName); + try { + FileInputStream inputStream = new FileInputStream(file); + return inputStream; + } catch (FileNotFoundException e) { + throw new SOPGPException.MissingInput(String.format(ERROR_INPUT_NOT_EXIST, fileName), e); + } + } + + public static File createNewFileOrThrow(File file) throws IOException { + if (file == null) { + throw new NullPointerException("File cannot be null."); + } + + try { + if (!file.createNewFile()) { + throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_EXISTS, file.getAbsolutePath())); + } + } catch (IOException e) { + throw new IOException(String.format(ERROR_CANNOT_CREATE_FILE, file.getAbsolutePath(), e.getMessage())); + } + return file; + } +} diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java index 4087b60c..e4b62e3c 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/ArmorCmd.java @@ -33,21 +33,9 @@ public class ArmorCmd implements Runnable { @CommandLine.Option(names = {"--label"}, description = "Label to be used in the header and tail of the armoring.", paramLabel = "{auto|sig|key|cert|message}") ArmorLabel label; - @CommandLine.Option(names = {"--allow-nested"}, description = "Allow additional armoring of already armored input") - boolean allowNested = false; - @Override public void run() { Armor armor = SopCLI.getSop().armor(); - if (allowNested) { - try { - armor.allowNested(); - } catch (SOPGPException.UnsupportedOption unsupportedOption) { - Print.errln("Option --allow-nested is not supported."); - Print.trace(unsupportedOption); - System.exit(unsupportedOption.getExitCode()); - } - } if (label != null) { try { armor.label(label); diff --git a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java index b4c4bccd..21a90a06 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/commands/DecryptCmd.java @@ -32,6 +32,7 @@ import sop.ReadyWithResult; import sop.SessionKey; import sop.Verification; import sop.cli.picocli.DateParser; +import sop.cli.picocli.FileUtil; import sop.cli.picocli.SopCLI; import sop.exception.SOPGPException; import sop.operation.Decrypt; @@ -42,8 +43,15 @@ import sop.util.HexUtil; exitCodeOnInvalidInput = SOPGPException.UnsupportedOption.EXIT_CODE) public class DecryptCmd implements Runnable { + private static final String SESSION_KEY_OUT = "--session-key-out"; + private static final String VERIFY_OUT = "--verify-out"; + + private static final String ERROR_UNSUPPORTED_OPTION = "Option '%s' is not supported."; + private static final String ERROR_FILE_NOT_EXIST = "File '%s' does not exist."; + private static final String ERROR_OUTPUT_OF_OPTION_EXISTS = "Target %s of option %s already exists."; + @CommandLine.Option( - names = {"--session-key-out"}, + names = {SESSION_KEY_OUT}, description = "Can be used to learn the session key on successful decryption", paramLabel = "SESSIONKEY") File sessionKeyOut; @@ -60,7 +68,7 @@ public class DecryptCmd implements Runnable { paramLabel = "PASSWORD") List withPassword = new ArrayList<>(); - @CommandLine.Option(names = {"--verify-out"}, + @CommandLine.Option(names = {VERIFY_OUT}, description = "Produces signature verification status to the designated file", paramLabel = "VERIFICATIONS") File verifyOut; @@ -92,7 +100,8 @@ public class DecryptCmd implements Runnable { @Override public void run() { - throwIfVerifyOutExists(verifyOut); + throwIfOutputExists(verifyOut, VERIFY_OUT); + throwIfOutputExists(sessionKeyOut, SESSION_KEY_OUT); Decrypt decrypt = SopCLI.getSop().decrypt(); if (decrypt == null) { @@ -107,7 +116,8 @@ public class DecryptCmd implements Runnable { setDecryptWith(keys, decrypt); if (verifyOut != null && certs.isEmpty()) { - throw new SOPGPException.IncompleteVerification("--verify-out is requested, but no --verify-with was provided."); + String errorMessage = "Option %s is requested, but no option %s was provided."; + throw new SOPGPException.IncompleteVerification(String.format(errorMessage, VERIFY_OUT, "--verify-with")); } try { @@ -122,11 +132,19 @@ public class DecryptCmd implements Runnable { } } + private void throwIfOutputExists(File outputFile, String optionName) { + if (outputFile == null) { + return; + } + + if (outputFile.exists()) { + throw new SOPGPException.OutputExists(String.format(ERROR_OUTPUT_OF_OPTION_EXISTS, outputFile.getAbsolutePath(), optionName)); + } + } + private void writeVerifyOut(DecryptionResult result) throws IOException { if (verifyOut != null) { - if (!verifyOut.createNewFile()) { - throw new IOException("Cannot create file " + verifyOut.getAbsolutePath()); - } + FileUtil.createNewFileOrThrow(verifyOut); try (FileOutputStream outputStream = new FileOutputStream(verifyOut)) { PrintWriter writer = new PrintWriter(outputStream); for (Verification verification : result.getVerifications()) { @@ -141,9 +159,7 @@ public class DecryptCmd implements Runnable { private void writeSessionKeyOut(DecryptionResult result) throws IOException { if (sessionKeyOut != null) { - if (sessionKeyOut.exists()) { - throw new SOPGPException.OutputExists("Target " + sessionKeyOut.getAbsolutePath() + " of option --session-key-out already exists."); - } + FileUtil.createNewFileOrThrow(sessionKeyOut); try (FileOutputStream outputStream = new FileOutputStream(sessionKeyOut)) { if (!result.getSessionKey().isPresent()) { @@ -157,16 +173,6 @@ public class DecryptCmd implements Runnable { } } - private void throwIfVerifyOutExists(File verifyOut) throws SOPGPException.OutputExists { - if (verifyOut == null) { - return; - } - - if (verifyOut.exists()) { - throw new SOPGPException.OutputExists("Target " + verifyOut.getAbsolutePath() + " of option --verify-out already exists."); - } - } - private void setDecryptWith(List keys, Decrypt decrypt) { for (File key : keys) { try (FileInputStream keyIn = new FileInputStream(key)) { @@ -176,7 +182,7 @@ public class DecryptCmd implements Runnable { } catch (SOPGPException.BadData badData) { throw new SOPGPException.BadData("File " + key.getAbsolutePath() + " does not contain a private key.", badData); } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput("File " + key.getAbsolutePath() + " does not exist.", e); + throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, key.getAbsolutePath()), e); } catch (IOException e) { throw new RuntimeException(e); } @@ -188,7 +194,7 @@ public class DecryptCmd implements Runnable { try (FileInputStream certIn = new FileInputStream(cert)) { decrypt.verifyWithCert(certIn); } catch (FileNotFoundException e) { - throw new SOPGPException.MissingInput("File " + cert.getAbsolutePath() + " does not exist.", e); + throw new SOPGPException.MissingInput(String.format(ERROR_FILE_NOT_EXIST, cert.getAbsolutePath()), e); } catch (SOPGPException.BadData badData) { throw new SOPGPException.BadData("File " + cert.getAbsolutePath() + " does not contain a valid certificate.", badData); } catch (IOException ioException) { @@ -210,7 +216,7 @@ public class DecryptCmd implements Runnable { try { decrypt.withSessionKey(new SessionKey(algorithm, key)); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Unsupported option '--with-session-key'.", unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-session-key"), unsupportedOption); } } } @@ -220,7 +226,7 @@ public class DecryptCmd implements Runnable { try { decrypt.withPassword(password); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption); } } } @@ -230,7 +236,7 @@ public class DecryptCmd implements Runnable { try { decrypt.verifyNotAfter(notAfterDate); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Option '--not-after' not supported.", unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-after"), unsupportedOption); } } @@ -239,7 +245,7 @@ public class DecryptCmd implements Runnable { try { decrypt.verifyNotBefore(notBeforeDate); } catch (SOPGPException.UnsupportedOption unsupportedOption) { - throw new SOPGPException.UnsupportedOption("Option '--not-before' not supported.", unsupportedOption); + throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--not-before"), unsupportedOption); } } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java index 0f7b232b..17d32536 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/ArmorCmdTest.java @@ -51,18 +51,6 @@ public class ArmorCmdTest { SopCLI.setSopInstance(sop); } - @Test - public void assertAllowNestedIsCalledWhenFlagged() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"armor", "--allow-nested"}); - verify(armor, times(1)).allowNested(); - } - - @Test - public void assertAllowNestedIsNotCalledByDefault() throws SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"armor"}); - verify(armor, never()).allowNested(); - } - @Test public void assertLabelIsNotCalledByDefault() throws SOPGPException.UnsupportedOption { SopCLI.main(new String[] {"armor"}); @@ -97,14 +85,6 @@ public class ArmorCmdTest { SopCLI.main(new String[] {"armor", "--label", "Sig"}); } - @Test - @ExpectSystemExitWithStatus(37) - public void ifAllowNestedUnsupportedExit37() throws SOPGPException.UnsupportedOption { - when(armor.allowNested()).thenThrow(new SOPGPException.UnsupportedOption("Allowing nested Armor not supported.")); - - SopCLI.main(new String[] {"armor", "--allow-nested"}); - } - @Test @ExpectSystemExitWithStatus(41) public void ifBadDataExit41() throws SOPGPException.BadData { diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java index b1df995e..77091d1c 100644 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -253,6 +253,10 @@ public abstract class SOPGPException extends RuntimeException { public static final int EXIT_CODE = 73; + public AmbiguousInput(String message) { + super(message); + } + @Override public int getExitCode() { return EXIT_CODE; diff --git a/sop-java/src/main/java/sop/operation/Armor.java b/sop-java/src/main/java/sop/operation/Armor.java index 59db198a..b60bf59b 100644 --- a/sop-java/src/main/java/sop/operation/Armor.java +++ b/sop-java/src/main/java/sop/operation/Armor.java @@ -31,13 +31,6 @@ public interface Armor { */ Armor label(ArmorLabel label) throws SOPGPException.UnsupportedOption; - /** - * Allow nested Armoring. - * - * @return builder instance - */ - Armor allowNested() throws SOPGPException.UnsupportedOption; - /** * Armor the provided data. *