From 47627b92b4e1d101c6ebfe67a20cd4fd1d59d014 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 23 Jul 2025 12:39:04 +1000 Subject: [PATCH 1/3] Basic tests for SeqCliEncryptionProviderConfig and ExternalEncryptionProvider --- .../Config/SeqCliEncryptionProviderConfig.cs | 11 +++- src/SeqCli/Encryptor/ExternalDataProtector.cs | 16 +++--- src/SeqCli/SeqCli.csproj | 8 +-- .../SeqCli.EndToEnd/Support/TestDataFolder.cs | 2 +- .../Config/ExternalDataProtectorTests.cs | 57 +++++++++++++++++++ .../SeqCliEncryptionProviderConfigTests.cs | 54 ++++++++++++++++++ test/SeqCli.Tests/SeqCli.Tests.csproj | 15 +++++ test/SeqCli.Tests/Support/TempFolder.cs | 2 +- 8 files changed, 149 insertions(+), 16 deletions(-) create mode 100644 test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs create mode 100644 test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs diff --git a/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs b/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs index e7273707..6eeddb37 100644 --- a/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs +++ b/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; using SeqCli.Encryptor; namespace SeqCli.Config; @@ -29,9 +30,15 @@ public IDataProtector DataProtector() #if WINDOWS return new WindowsNativeDataProtector(); #else - if (!string.IsNullOrWhiteSpace(Encryptor) && !string.IsNullOrWhiteSpace(Decryptor)) + if (!string.IsNullOrWhiteSpace(Encryptor) || !string.IsNullOrWhiteSpace(Decryptor)) { - return new ExternalDataProtector(this); + if (string.IsNullOrWhiteSpace(Encryptor) || string.IsNullOrWhiteSpace(Decryptor)) + { + throw new ArgumentException( + "If either of `encryption.encryptor` or `encryption.decryptor` is specified, both must be specified."); + } + + return new ExternalDataProtector(Encryptor, EncryptorArgs, Decryptor, DecryptorArgs); } return new PlaintextDataProtector(); diff --git a/src/SeqCli/Encryptor/ExternalDataProtector.cs b/src/SeqCli/Encryptor/ExternalDataProtector.cs index 0c84988b..e5a34905 100644 --- a/src/SeqCli/Encryptor/ExternalDataProtector.cs +++ b/src/SeqCli/Encryptor/ExternalDataProtector.cs @@ -9,13 +9,12 @@ namespace SeqCli.Encryptor; class ExternalDataProtector : IDataProtector { - public ExternalDataProtector(SeqCliEncryptionProviderConfig providerConfig) + public ExternalDataProtector(string encryptor, string? encryptorArgs, string decryptor, string? decryptorArgs) { - _encryptor = providerConfig.Encryptor!; - _encryptorArgs = providerConfig.EncryptorArgs; - - _decryptor = providerConfig.Decryptor!; - _decryptorArgs = providerConfig.DecryptorArgs; + _encryptor = encryptor ?? throw new ArgumentNullException(nameof(encryptor)); + _encryptorArgs = encryptorArgs; + _decryptor = decryptor ?? throw new ArgumentNullException(nameof(decryptor)); + _decryptorArgs = decryptorArgs; } readonly string _encryptor; @@ -28,7 +27,7 @@ public byte[] Encrypt(byte[] unencrypted) var exit = Invoke(_encryptor, _encryptorArgs, unencrypted, out var encrypted, out var err); if (exit != 0) { - throw new Exception($"Encryptor failed with exit code {exit} and produced: {err}"); + throw new Exception($"Encryptor failed with exit code {exit} and produced: {err}."); } return encrypted; @@ -39,7 +38,7 @@ public byte[] Decrypt(byte[] encrypted) var exit = Invoke(_decryptor, _decryptorArgs, encrypted, out var decrypted, out var err); if (exit != 0) { - throw new Exception($"Decryptor failed with exit code {exit} and produced: {err}"); + throw new Exception($"Decryptor failed with exit code {exit} and produced: {err}."); } return decrypted; @@ -50,6 +49,7 @@ static int Invoke(string fullExePath, string? args, byte[] stdin, out byte[] std var startInfo = new ProcessStartInfo { UseShellExecute = false, + RedirectStandardInput = true, RedirectStandardError = true, RedirectStandardOutput = true, WindowStyle = ProcessWindowStyle.Hidden, diff --git a/src/SeqCli/SeqCli.csproj b/src/SeqCli/SeqCli.csproj index e7ef671e..b62c6f98 100644 --- a/src/SeqCli/SeqCli.csproj +++ b/src/SeqCli/SeqCli.csproj @@ -22,16 +22,16 @@ true - WINDOWS + $(DefineConstants);WINDOWS - OSX + $(DefineConstants);OSX - LINUX + $(DefineConstants);LINUX - UNIX + $(DefineConstants);UNIX diff --git a/test/SeqCli.EndToEnd/Support/TestDataFolder.cs b/test/SeqCli.EndToEnd/Support/TestDataFolder.cs index fd4e106e..8de84187 100644 --- a/test/SeqCli.EndToEnd/Support/TestDataFolder.cs +++ b/test/SeqCli.EndToEnd/Support/TestDataFolder.cs @@ -11,7 +11,7 @@ public TestDataFolder() { _basePath = System.IO.Path.Combine( System.IO.Path.GetTempPath(), - "SeqCli Test", + "SeqCli.Tests.EndToEnd", Guid.NewGuid().ToString("n")); Directory.CreateDirectory(_basePath); diff --git a/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs b/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs new file mode 100644 index 00000000..d89865bb --- /dev/null +++ b/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs @@ -0,0 +1,57 @@ +using System; +using System.ComponentModel; +using System.IO; +using System.Text; +using SeqCli.Encryptor; +using SeqCli.Tests.Support; +using Xunit; + +namespace SeqCli.Tests.Config; + +public class ExternalDataProtectorTests +{ + [Fact] + public void IfEncryptorDoesNotExistEncryptThrows() + { + var protector = new ExternalDataProtector(Some.String(), null, Some.String(), null); + Assert.Throws(() => protector.Encrypt(Some.Bytes(200))); + } + +#if UNIX + [Fact] + public void IfEncryptorFailsEncryptThrows() + { + var protector = new ExternalDataProtector("bash", "-c \"exit 1\"", Some.String(), null); + Assert.Throws(() => protector.Encrypt(Some.Bytes(200))); + } + + [Fact] + public void EncryptCallsEncryptor() + { + const string prefix = "123"; + + var encoding = new UTF8Encoding(false); + using var temp = TempFolder.ForCaller(); + var filename = temp.AllocateFilename(); + File.WriteAllBytes(filename, encoding.GetBytes(prefix)); + + const string input = "Hello, world!"; + + var protector = new ExternalDataProtector("bash", $"-c \"cat '{filename}' -\"", Some.String(), null); + var actual = encoding.GetString(protector.Encrypt(encoding.GetBytes(input))); + + Assert.Equal($"{prefix}{input}", actual); + } + + [Fact] + public void EncryptionRoundTrips() + { + const string echo = "bash"; + const string echoArgs = "-c \"cat -\""; + var protector = new ExternalDataProtector(echo, echoArgs, echo, echoArgs); + var expected = Some.Bytes(200); + var actual = protector.Decrypt(protector.Encrypt(expected)); + Assert.Equal(expected, actual); + } +#endif +} diff --git a/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs b/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs new file mode 100644 index 00000000..21bb7c8c --- /dev/null +++ b/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.InteropServices; +using SeqCli.Config; +using SeqCli.Encryptor; +using Xunit; + +namespace SeqCli.Tests.Config; + +public class SeqCliEncryptionProviderConfigTests +{ +#if WINDOWS + [Fact] + public void DefaultDataProtectorOnWindowsIsDpapi() + { + Assert.True(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + var config = new SeqCliEncryptionProviderConfig(); + var provider = config.DataProtector(); + Assert.IsType(provider); + } +#else + [Fact] + public void DefaultDataProtectorOnUnixIsPlaintext() + { + Assert.False(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)); + + var config = new SeqCliEncryptionProviderConfig(); + var provider = config.DataProtector(); + Assert.IsType(provider); + } +#endif + + [Fact] + public void SpecifyingEncryptorRequiresDecryptor() + { + var config = new SeqCliEncryptionProviderConfig + { + Encryptor = "test" + }; + + Assert.Throws(() => config.DataProtector()); + } + + [Fact] + public void SpecifyingDecryptorRequiresEncryptor() + { + var config = new SeqCliEncryptionProviderConfig + { + Decryptor = "test" + }; + + Assert.Throws(() => config.DataProtector()); + } +} \ No newline at end of file diff --git a/test/SeqCli.Tests/SeqCli.Tests.csproj b/test/SeqCli.Tests/SeqCli.Tests.csproj index b9f7df2d..73d17553 100644 --- a/test/SeqCli.Tests/SeqCli.Tests.csproj +++ b/test/SeqCli.Tests/SeqCli.Tests.csproj @@ -2,6 +2,21 @@ net9.0 $(TargetFrameworks);net9.0-windows + true + true + true + + + $(DefineConstants);WINDOWS + + + $(DefineConstants);OSX + + + $(DefineConstants);LINUX + + + $(DefineConstants);UNIX diff --git a/test/SeqCli.Tests/Support/TempFolder.cs b/test/SeqCli.Tests/Support/TempFolder.cs index 8a5e85a0..63a7e18b 100644 --- a/test/SeqCli.Tests/Support/TempFolder.cs +++ b/test/SeqCli.Tests/Support/TempFolder.cs @@ -15,7 +15,7 @@ public TempFolder(string name) { Path = System.IO.Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "SeqCli.Forwarder.Tests", + "SeqCli.Tests", Session.ToString("n"), name); From bc571f5caaabc5570a1637c652186ee0062e9e06 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 23 Jul 2025 12:51:11 +1000 Subject: [PATCH 2/3] Allow use of external data protectors on Windows --- src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs | 6 +++--- .../Config/SeqCliEncryptionProviderConfigTests.cs | 12 ++++++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs b/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs index 6eeddb37..492b4a16 100644 --- a/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs +++ b/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs @@ -27,9 +27,6 @@ class SeqCliEncryptionProviderConfig public IDataProtector DataProtector() { -#if WINDOWS - return new WindowsNativeDataProtector(); -#else if (!string.IsNullOrWhiteSpace(Encryptor) || !string.IsNullOrWhiteSpace(Decryptor)) { if (string.IsNullOrWhiteSpace(Encryptor) || string.IsNullOrWhiteSpace(Decryptor)) @@ -41,6 +38,9 @@ public IDataProtector DataProtector() return new ExternalDataProtector(Encryptor, EncryptorArgs, Decryptor, DecryptorArgs); } +#if WINDOWS + return new WindowsNativeDataProtector(); +#else return new PlaintextDataProtector(); #endif } diff --git a/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs b/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs index 21bb7c8c..3f171c97 100644 --- a/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs +++ b/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs @@ -51,4 +51,16 @@ public void SpecifyingDecryptorRequiresEncryptor() Assert.Throws(() => config.DataProtector()); } + + [Fact] + public void SpecifyingEncryptorAndDecryptorActivatesExternalDataProtector() + { + var config = new SeqCliEncryptionProviderConfig + { + Encryptor = "test", + Decryptor = "test" + }; + + Assert.IsType(config.DataProtector()); + } } \ No newline at end of file From 36313d763a83f9280799ca9ffd6496b9e07bae3a Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 23 Jul 2025 12:55:54 +1000 Subject: [PATCH 3/3] Be less picky about exception types when commands fail --- test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs b/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs index d89865bb..1b14fa4c 100644 --- a/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs +++ b/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs @@ -22,7 +22,8 @@ public void IfEncryptorDoesNotExistEncryptThrows() public void IfEncryptorFailsEncryptThrows() { var protector = new ExternalDataProtector("bash", "-c \"exit 1\"", Some.String(), null); - Assert.Throws(() => protector.Encrypt(Some.Bytes(200))); + // May be `Exception` or `IOException`. + Assert.ThrowsAny(() => protector.Encrypt(Some.Bytes(200))); } [Fact]