diff --git a/source/Octostache.Tests/UsageFixture.cs b/source/Octostache.Tests/UsageFixture.cs index c053d4b..72ffe97 100644 --- a/source/Octostache.Tests/UsageFixture.cs +++ b/source/Octostache.Tests/UsageFixture.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; using Octostache.Templates; using Xunit; @@ -581,5 +583,37 @@ public void CanGetIndexes() absentIndexes.Should().HaveCount(0); } + + [Fact] + public void EncryptText() + { + var certificate = MakeCert(); + if (certificate != null) + { + var variableDictionary = new VariableDictionary + { + //TODO: Verify this is how Octopus certificate variables create certificate base64 strings. #{CertificateVariable.Certificate} + ["CertificateBase64String"] = Convert.ToBase64String(certificate.RawData), + ["Data"] = "Test", + ["EncryptedData"] = "#{Data | RSAEncrypt #{CertificateBase64String}}" + }; + + var encryptedString = variableDictionary.Evaluate("#{EncryptedData}"); + encryptedString.Should().NotBeNullOrWhiteSpace(); + } + } + + static X509Certificate2 MakeCert() + { +#if NETCOREAPP + var rsa = RSA.Create(); + var req = new CertificateRequest("cn=test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + return req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); +#else + //TODO: could potentially manually create certificate and include it as an embedded resource to use here. + return null; +#endif + + } } } diff --git a/source/Octostache/Templates/BuiltInFunctions.cs b/source/Octostache/Templates/BuiltInFunctions.cs index 2bf284d..1e59178 100644 --- a/source/Octostache/Templates/BuiltInFunctions.cs +++ b/source/Octostache/Templates/BuiltInFunctions.cs @@ -26,7 +26,8 @@ static class BuiltInFunctions {"substring", TextSubstringFunction.Substring}, {"truncate", TextManipulationFunction.Truncate}, {"trim", TextManipulationFunction.Trim}, - {"uripart", TextManipulationFunction.UriPart} + {"uripart", TextManipulationFunction.UriPart}, + {"rsaencrypt", TextEncryptFunction.RSAEncrypt } }; // Configuration should be done at startup, this isn't thread-safe. diff --git a/source/Octostache/Templates/Functions/TextEncryptFunction.cs b/source/Octostache/Templates/Functions/TextEncryptFunction.cs new file mode 100644 index 0000000..c5d28c6 --- /dev/null +++ b/source/Octostache/Templates/Functions/TextEncryptFunction.cs @@ -0,0 +1,55 @@ +using System; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace Octostache.Templates.Functions +{ + internal class TextEncryptFunction + { + public static string RSAEncrypt(string data, string[] arguments) + { + if (arguments == null || arguments.Length == 0 || arguments.Length > 2) + { + return null; + } + + X509Certificate2 certificate = null; + + try + { + + certificate = new X509Certificate2(Convert.FromBase64String(arguments[0])); +#if NET40 + var publicKey = (RSACryptoServiceProvider)certificate.PublicKey.Key; +#else + var publicKey = certificate.GetRSAPublicKey(); +#endif + using (publicKey) + { + var encoding = Encoding.GetEncoding(arguments.Length > 1 ? arguments[1] : "utf-8"); + var byteData = encoding.GetBytes(data); +#if NET40 + var encryptedBytes = publicKey.Encrypt(byteData, true); +#else + var encryptedBytes = publicKey.Encrypt(byteData, RSAEncryptionPadding.OaepSHA1); +#endif + return Convert.ToBase64String(encryptedBytes); + } + } + catch (Exception) + { + } + finally + { + // IDisposable was only added to certificate after.Net 4.6 + if (certificate != null && certificate is IDisposable disposableCertificate) + { + disposableCertificate.Dispose(); + } + } + + return null; + } + } +} \ No newline at end of file