From 117117b7355513ef27c69868c860e831933b6040 Mon Sep 17 00:00:00 2001 From: Paul Schaub Date: Wed, 9 Feb 2022 15:12:23 +0100 Subject: [PATCH] Treat password and sessionkkey arguments as indirect data types --- CHANGELOG.md | 3 ++ .../main/java/sop/cli/picocli/FileUtil.java | 16 +++++++ .../sop/cli/picocli/commands/DecryptCmd.java | 17 +++++-- .../sop/cli/picocli/commands/EncryptCmd.java | 10 ++-- .../java/sop/cli/picocli/TestFileUtil.java | 29 +++++++++++ .../cli/picocli/commands/DecryptCmdTest.java | 31 +++++++----- .../cli/picocli/commands/EncryptCmdTest.java | 48 +++++++++++-------- .../java/sop/exception/SOPGPException.java | 22 +++++++-- 8 files changed, 134 insertions(+), 42 deletions(-) create mode 100644 sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 558b63a..d2db168 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## 1.2.0 +- `encrypt`, `decrypt`: Interpret arguments of `--with-password` and `--with-session-key` as indirect data types (e.g. file references instead of strings) + ## 1.1.0 - Initial release from new repository - Implement SOP specification version 3 \ No newline at end of file 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 index cd92e6d..3d4ac07 100644 --- a/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java +++ b/sop-java-picocli/src/main/java/sop/cli/picocli/FileUtil.java @@ -4,10 +4,13 @@ package sop.cli.picocli; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.InputStream; import java.io.IOException; +import java.nio.charset.Charset; import sop.exception.SOPGPException; @@ -95,4 +98,17 @@ public class FileUtil { } return file; } + + public static String stringFromInputStream(InputStream inputStream) throws IOException { + try { + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + byte[] buf = new byte[4096]; int read; + while ((read = inputStream.read(buf)) != -1) { + byteOut.write(buf, 0, read); + } + return new String(byteOut.toByteArray(), Charset.forName("UTF8")); + } finally { + inputStream.close(); + } + } } 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 8fc4650..090276f 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 @@ -47,13 +47,13 @@ public class DecryptCmd implements Runnable { @CommandLine.Option( names = {"--with-session-key"}, - description = "Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", + description = "Provide a session key file. Enables decryption of the \"CIPHERTEXT\" using the session key directly against the \"SEIPD\" packet", paramLabel = "SESSIONKEY") List withSessionKey = new ArrayList<>(); @CommandLine.Option( names = {"--with-password"}, - description = "Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", + description = "Provide a password file. Enables decryption based on any \"SKESK\" packets in the \"CIPHERTEXT\"", paramLabel = "PASSWORD") List withPassword = new ArrayList<>(); @@ -194,7 +194,13 @@ public class DecryptCmd implements Runnable { private void setWithSessionKeys(List withSessionKey, Decrypt decrypt) { Pattern sessionKeyPattern = Pattern.compile("^\\d+:[0-9A-F]+$"); - for (String sessionKey : withSessionKey) { + for (String sessionKeyFile : withSessionKey) { + String sessionKey; + try { + sessionKey = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(sessionKeyFile)); + } catch (IOException e) { + throw new SOPGPException.BadData("Cannot read session key from session key file " + sessionKeyFile, e); + } if (!sessionKeyPattern.matcher(sessionKey).matches()) { throw new IllegalArgumentException("Session keys are expected in the format 'ALGONUM:HEXKEY'."); } @@ -211,11 +217,14 @@ public class DecryptCmd implements Runnable { } private void setWithPasswords(List withPassword, Decrypt decrypt) { - for (String password : withPassword) { + for (String passwordFile : withPassword) { try { + String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFile)); decrypt.withPassword(password); } catch (SOPGPException.UnsupportedOption unsupportedOption) { throw new SOPGPException.UnsupportedOption(String.format(ERROR_UNSUPPORTED_OPTION, "--with-password"), unsupportedOption); + } catch (IOException e) { + throw new SOPGPException.PasswordNotHumanReadable("Cannot read password from password file " + passwordFile, 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 0634240..47447d2 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 @@ -13,6 +13,7 @@ import java.util.List; import picocli.CommandLine; import sop.Ready; +import sop.cli.picocli.FileUtil; import sop.cli.picocli.SopCLI; import sop.enums.EncryptAs; import sop.exception.SOPGPException; @@ -34,7 +35,7 @@ public class EncryptCmd implements Runnable { EncryptAs type; @CommandLine.Option(names = "--with-password", - description = "Encrypt the message with a password", + description = "Encrypt the message with a password provided by the given password file", paramLabel = "PASSWORD") List withPassword = new ArrayList<>(); @@ -64,14 +65,17 @@ public class EncryptCmd implements Runnable { } if (withPassword.isEmpty() && certs.isEmpty()) { - throw new SOPGPException.MissingArg("At least one password or cert file required for encryption."); + throw new SOPGPException.MissingArg("At least one password file or cert file required for encryption."); } - for (String password : withPassword) { + for (String passwordFileName : withPassword) { try { + String password = FileUtil.stringFromInputStream(FileUtil.getFileInputStream(passwordFileName)); encrypt.withPassword(password); } catch (SOPGPException.UnsupportedOption unsupportedOption) { throw new SOPGPException.UnsupportedOption("Unsupported option '--with-password'.", unsupportedOption); + } catch (IOException e) { + throw new SOPGPException.PasswordNotHumanReadable("Cannot read password from the provided password file " + passwordFileName, e); } } diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java b/sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java new file mode 100644 index 0000000..385fe1a --- /dev/null +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/TestFileUtil.java @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2022 Paul Schaub +// +// SPDX-License-Identifier: Apache-2.0 + +package sop.cli.picocli; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +public class TestFileUtil { + + public static File writeTempStringFile(String string) throws IOException { + File tempDir = Files.createTempDirectory("tmpDir").toFile(); + tempDir.deleteOnExit(); + tempDir.mkdirs(); + + File passwordFile = new File(tempDir, "file"); + passwordFile.createNewFile(); + + FileOutputStream fileOut = new FileOutputStream(passwordFile); + fileOut.write(string.getBytes(StandardCharsets.UTF_8)); + fileOut.close(); + + return passwordFile; + } +} diff --git a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java index 9e1c35b..54a8485 100644 --- a/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java +++ b/sop-java-picocli/src/test/java/sop/cli/picocli/commands/DecryptCmdTest.java @@ -38,6 +38,7 @@ import sop.SessionKey; import sop.Verification; import sop.cli.picocli.DateParser; import sop.cli.picocli.SopCLI; +import sop.cli.picocli.TestFileUtil; import sop.exception.SOPGPException; import sop.operation.Decrypt; import sop.util.HexUtil; @@ -90,22 +91,25 @@ public class DecryptCmdTest { @Test @ExpectSystemExitWithStatus(31) public void assertNotHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, - SOPGPException.UnsupportedOption { + SOPGPException.UnsupportedOption, IOException { + File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - SopCLI.main(new String[] {"decrypt", "--with-password", "pretendThisIsNotReadable"}); + SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test - public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { - SopCLI.main(new String[] {"decrypt", "--with-password", "orange"}); + public void assertWithPasswordPassesPasswordDown() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + File passwordFile = TestFileUtil.writeTempStringFile("orange"); + SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); verify(decrypt, times(1)).withPassword("orange"); } @Test @ExpectSystemExitWithStatus(37) - public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { + public void assertUnsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { + File passwordFile = TestFileUtil.writeTempStringFile("swordfish"); when(decrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Decrypting with password not supported.")); - SopCLI.main(new String[] {"decrypt", "--with-password", "swordfish"}); + SopCLI.main(new String[] {"decrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test @@ -289,21 +293,26 @@ public class DecryptCmdTest { } @Test - public void assertWithSessionKeyIsPassedDown() throws SOPGPException.UnsupportedOption { + public void assertWithSessionKeyIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { SessionKey key1 = new SessionKey((byte) 9, HexUtil.hexToBytes("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137")); SessionKey key2 = new SessionKey((byte) 9, HexUtil.hexToBytes("FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD")); + + File sessionKeyFile1 = TestFileUtil.writeTempStringFile(key1.toString()); + File sessionKeyFile2 = TestFileUtil.writeTempStringFile(key2.toString()); + SopCLI.main(new String[] {"decrypt", - "--with-session-key", "9:C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137", - "--with-session-key", "9:FCA4BEAF687F48059CACC14FB019125CD57392BAB7037C707835925CBF9F7BCD"}); + "--with-session-key", sessionKeyFile1.getAbsolutePath(), + "--with-session-key", sessionKeyFile2.getAbsolutePath()}); verify(decrypt).withSessionKey(key1); verify(decrypt).withSessionKey(key2); } @Test @ExpectSystemExitWithStatus(1) - public void assertMalformedSessionKeysResultInExit1() { + public void assertMalformedSessionKeysResultInExit1() throws IOException { + File sessionKeyFile = TestFileUtil.writeTempStringFile("C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"); SopCLI.main(new String[] {"decrypt", - "--with-session-key", "C7CBDAF42537776F12509B5168793C26B93294E5ABDFA73224FB0177123E9137"}); + "--with-session-key", sessionKeyFile.getAbsolutePath()}); } @Test 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 91f0a1e..4edb5e6 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 @@ -22,6 +22,7 @@ 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; @@ -67,35 +68,36 @@ public class EncryptCmdTest { } @Test - public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption { + public void as_modeIsPassedDown() throws SOPGPException.UnsupportedOption, IOException { + File passwordFile = TestFileUtil.writeTempStringFile("0rbit"); for (EncryptAs mode : EncryptAs.values()) { - SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", "0rbit"}); + SopCLI.main(new String[] {"encrypt", "--as", mode.name(), "--with-password", passwordFile.getAbsolutePath()}); verify(encrypt, times(1)).mode(mode); } } @Test @ExpectSystemExitWithStatus(31) - public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { + public void withPassword_notHumanReadablePasswordCausesExit31() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword("pretendThisIsNotReadable")).thenThrow(new SOPGPException.PasswordNotHumanReadable()); - - SopCLI.main(new String[] {"encrypt", "--with-password", "pretendThisIsNotReadable"}); + File passwordFile = TestFileUtil.writeTempStringFile("pretendThisIsNotReadable"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test @ExpectSystemExitWithStatus(37) - public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption { + public void withPassword_unsupportedWithPasswordCausesExit37() throws SOPGPException.PasswordNotHumanReadable, SOPGPException.UnsupportedOption, IOException { when(encrypt.withPassword(any())).thenThrow(new SOPGPException.UnsupportedOption("Encrypting with password not supported.")); - - SopCLI.main(new String[] {"encrypt", "--with-password", "orange"}); + File passwordFile = TestFileUtil.writeTempStringFile("orange"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); } @Test public void signWith_multipleTimesGetPassedDown() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { File keyFile1 = File.createTempFile("sign-with-1-", ".asc"); File keyFile2 = File.createTempFile("sign-with-2-", ".asc"); - - SopCLI.main(new String[] {"encrypt", "--with-password", "password", "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); + File passwordFile = TestFileUtil.writeTempStringFile("password"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile1.getAbsolutePath(), "--sign-with", keyFile2.getAbsolutePath()}); verify(encrypt, times(2)).signWith((InputStream) any()); } @@ -110,7 +112,8 @@ public class EncryptCmdTest { public void signWith_keyIsProtectedCausesExit67() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyIsProtected()); File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", "starship"}); + File passwordFile = TestFileUtil.writeTempStringFile("starship"); + SopCLI.main(new String[] {"encrypt", "--sign-with", keyFile.getAbsolutePath(), "--with-password", passwordFile.getAbsolutePath()}); } @Test @@ -118,7 +121,8 @@ public class EncryptCmdTest { public void signWith_unsupportedAsymmetricAlgoCausesExit13() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.UnsupportedAsymmetricAlgo("Unsupported asymmetric algorithm.", new Exception())); File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "123456", "--sign-with", keyFile.getAbsolutePath()}); + File passwordFile = TestFileUtil.writeTempStringFile("123456"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); } @Test @@ -126,7 +130,8 @@ public class EncryptCmdTest { public void signWith_certCannotSignCausesExit1() throws IOException, SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.KeyCannotSign()); File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "dragon", "--sign-with", keyFile.getAbsolutePath()}); + File passwordFile = TestFileUtil.writeTempStringFile("dragon"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); } @Test @@ -134,7 +139,8 @@ public class EncryptCmdTest { public void signWith_badDataCausesExit41() throws SOPGPException.KeyIsProtected, SOPGPException.UnsupportedAsymmetricAlgo, SOPGPException.KeyCannotSign, SOPGPException.BadData, IOException { when(encrypt.signWith((InputStream) any())).thenThrow(new SOPGPException.BadData(new IOException())); File keyFile = File.createTempFile("sign-with", ".asc"); - SopCLI.main(new String[] {"encrypt", "--with-password", "orange", "--sign-with", keyFile.getAbsolutePath()}); + File passwordFile = TestFileUtil.writeTempStringFile("orange"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--sign-with", keyFile.getAbsolutePath()}); } @Test @@ -168,14 +174,16 @@ public class EncryptCmdTest { } @Test - public void noArmor_notCalledByDefault() { - SopCLI.main(new String[] {"encrypt", "--with-password", "clownfish"}); + public void noArmor_notCalledByDefault() throws IOException { + File passwordFile = TestFileUtil.writeTempStringFile("clownfish"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); verify(encrypt, never()).noArmor(); } @Test - public void noArmor_callGetsPassedDown() { - SopCLI.main(new String[] {"encrypt", "--with-password", "monkey", "--no-armor"}); + public void noArmor_callGetsPassedDown() throws IOException { + File passwordFile = TestFileUtil.writeTempStringFile("monkey"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath(), "--no-armor"}); verify(encrypt, times(1)).noArmor(); } @@ -188,7 +196,7 @@ public class EncryptCmdTest { throw new IOException(); } }); - - SopCLI.main(new String[] {"encrypt", "--with-password", "wildcat"}); + File passwordFile = TestFileUtil.writeTempStringFile("wildcat"); + SopCLI.main(new String[] {"encrypt", "--with-password", passwordFile.getAbsolutePath()}); } } diff --git a/sop-java/src/main/java/sop/exception/SOPGPException.java b/sop-java/src/main/java/sop/exception/SOPGPException.java index 6b844f5..6fa66d8 100644 --- a/sop-java/src/main/java/sop/exception/SOPGPException.java +++ b/sop-java/src/main/java/sop/exception/SOPGPException.java @@ -4,6 +4,8 @@ package sop.exception; +import java.io.IOException; + public abstract class SOPGPException extends RuntimeException { public SOPGPException() { @@ -128,6 +130,14 @@ public abstract class SOPGPException extends RuntimeException { public static final int EXIT_CODE = 31; + public PasswordNotHumanReadable() { + super(); + } + + public PasswordNotHumanReadable(String message, IOException e) { + super(message, e); + } + @Override public int getExitCode() { return EXIT_CODE; @@ -162,12 +172,16 @@ public abstract class SOPGPException extends RuntimeException { public static final int EXIT_CODE = 41; - public BadData(Throwable e) { - super(e); + public BadData(String message) { + super(message); } - public BadData(String message, BadData badData) { - super(message, badData); + public BadData(Throwable throwable) { + super(throwable); + } + + public BadData(String message, Throwable throwable) { + super(message, throwable); } @Override