diff --git a/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs b/src/SeqCli/Config/SeqCliEncryptionProviderConfig.cs index e7273707..492b4a16 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; @@ -26,14 +27,20 @@ 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)) { - 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); } +#if WINDOWS + return new WindowsNativeDataProtector(); +#else return new PlaintextDataProtector(); #endif } 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..1b14fa4c --- /dev/null +++ b/test/SeqCli.Tests/Config/ExternalDataProtectorTests.cs @@ -0,0 +1,58 @@ +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); + // May be `Exception` or `IOException`. + Assert.ThrowsAny(() => 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..3f171c97 --- /dev/null +++ b/test/SeqCli.Tests/Config/SeqCliEncryptionProviderConfigTests.cs @@ -0,0 +1,66 @@ +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()); + } + + [Fact] + public void SpecifyingEncryptorAndDecryptorActivatesExternalDataProtector() + { + var config = new SeqCliEncryptionProviderConfig + { + Encryptor = "test", + Decryptor = "test" + }; + + Assert.IsType(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);