From 885741cde863a41c4450d913cd1d087c1c3f3005 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Fri, 15 Aug 2014 09:25:19 +1000 Subject: [PATCH 1/2] add multi decryption key support remove redundant copy paste code Update ConfigureRijndaelEncryptionService.cs encryption tests got moved in 4.1 --- ...en_using_encryption_with_custom_service.cs | 75 +++++++++ .../When_using_encryption_with_multikey.cs | 110 +++++++++++++ .../NServiceBus.AcceptanceTests.csproj | 4 +- ...ConfigureRijndaelEncryptionServiceTests.cs | 140 +++++++++++++++++ .../Encryption/EncryptionServiceTests.cs | 81 ++++++++++ .../NServiceBus.Core.Tests.csproj | 2 + .../Config/RijndaelEncryptionServiceConfig.cs | 15 ++ .../Config/RijndaelExpiredKey.cs | 35 +++++ .../Config/RijndaelExpiredKeyCollection.cs | 148 ++++++++++++++++++ .../ConfigureRijndaelEncryptionService.cs | 36 +++++ .../Encryption/Rijndael/EncryptionService.cs | 40 ++++- src/NServiceBus.Core/NServiceBus.Core.csproj | 2 + 12 files changed, 681 insertions(+), 7 deletions(-) create mode 100644 src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_custom_service.cs create mode 100644 src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_multikey.cs create mode 100644 src/NServiceBus.Core.Tests/Encryption/ConfigureRijndaelEncryptionServiceTests.cs create mode 100644 src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs create mode 100644 src/NServiceBus.Core/Config/RijndaelExpiredKey.cs create mode 100644 src/NServiceBus.Core/Config/RijndaelExpiredKeyCollection.cs diff --git a/src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_custom_service.cs b/src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_custom_service.cs new file mode 100644 index 00000000000..136bbd5f1e0 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_custom_service.cs @@ -0,0 +1,75 @@ +namespace NServiceBus.AcceptanceTests.Encryption +{ + using System.Linq; + using NServiceBus.Encryption; + using System; + using EndpointTemplates; + using AcceptanceTesting; + using NUnit.Framework; + using ScenarioDescriptors; + + public class When_using_encryption_with_custom_service : NServiceBusAcceptanceTest + { + [Test] + public void Should_receive_decrypted_message() + { + Scenario.Define() + .WithEndpoint(b => b.Given((bus, context) => bus.SendLocal(new MessageWithSecretData + { + Secret = "betcha can't guess my secret" + }))) + .Done(c => c.Done) + .Repeat(r => r.For()) + .Should(c => Assert.AreEqual("betcha can't guess my secret", c.Secret)) + .Run(); + } + + public class Context : ScenarioContext + { + public bool Done { get; set; } + public string Secret { get; set; } + } + + public class Endpoint : EndpointConfigurationBuilder + { + public Endpoint() + { + EndpointSetup(c => c.Configurer.RegisterSingleton(new MyEncryptionService())); + } + + public class Handler : IHandleMessages + { + public Context Context { get; set; } + + public void Handle(MessageWithSecretData message) + { + Context.Secret = message.Secret.Value; + Context.Done = true; + } + } + } + + [Serializable] + public class MessageWithSecretData : IMessage + { + public WireEncryptedString Secret { get; set; } + } + + + public class MyEncryptionService : IEncryptionService + { + public EncryptedValue Encrypt(string value) + { + return new EncryptedValue + { + EncryptedBase64Value = new string(value.Reverse().ToArray()) + }; + } + + public string Decrypt(EncryptedValue encryptedValue) + { + return new string(encryptedValue.EncryptedBase64Value.Reverse().ToArray()); + } + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_multikey.cs b/src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_multikey.cs new file mode 100644 index 00000000000..60dbf90ce99 --- /dev/null +++ b/src/NServiceBus.AcceptanceTests/Encryption/When_using_encryption_with_multikey.cs @@ -0,0 +1,110 @@ +namespace NServiceBus.AcceptanceTests.Encryption +{ + using System; + using Config; + using Config.ConfigurationSource; + using EndpointTemplates; + using AcceptanceTesting; + using NUnit.Framework; + using ScenarioDescriptors; + + public class When_using_encryption_with_multikey : NServiceBusAcceptanceTest + { + [Test] + public void Should_receive_decrypted_message() + { + Scenario.Define() + .WithEndpoint(b => b.Given((bus, context) => bus.Send(new MessageWithSecretData + { + Secret = "betcha can't guess my secret", + }))) + .WithEndpoint() + .Done(c => c.Done) + .Repeat(r => r.For()) + .Should(c => Assert.AreEqual("betcha can't guess my secret", c.Secret)) + .Run(); + } + + public class Context : ScenarioContext + { + public bool Done { get; set; } + + public string Secret { get; set; } + } + + public class Sender : EndpointConfigurationBuilder + { + public Sender() + { + EndpointSetup(c => c.RijndaelEncryptionService()) + .AddMapping(typeof(Receiver)); + } + + public class Handler : IHandleMessages + { + public Context Context { get; set; } + + public void Handle(MessageWithSecretData message) + { + Context.Secret = message.Secret.Value; + Context.Done = true; + } + } + + public class ConfigureEncryption : IProvideConfiguration + { + public RijndaelEncryptionServiceConfig GetConfiguration() + { + return new RijndaelEncryptionServiceConfig + { + Key = "gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6" + }; + } + } + } + + public class Receiver : EndpointConfigurationBuilder + { + public Receiver() + { + EndpointSetup(c => c.RijndaelEncryptionService()); + } + + public class Handler : IHandleMessages + { + public Context Context { get; set; } + + public void Handle(MessageWithSecretData message) + { + Context.Secret = message.Secret.Value; + Context.Done = true; + } + } + + public class ConfigureEncryption : IProvideConfiguration + { + public RijndaelEncryptionServiceConfig GetConfiguration() + { + return new RijndaelEncryptionServiceConfig + { + Key = "adDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6", + ExpiredKeys = new RijndaelExpiredKeyCollection + { + new RijndaelExpiredKey + { + Key = "gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6" + } + } + }; + } + } + } + + [Serializable] + public class MessageWithSecretData : IMessage + { + public WireEncryptedString Secret { get; set; } + } + + } +} \ No newline at end of file diff --git a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj index ce50b3e1ac0..e8164a91dac 100644 --- a/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj +++ b/src/NServiceBus.AcceptanceTests/NServiceBus.AcceptanceTests.csproj @@ -1,4 +1,4 @@ - + @@ -115,6 +115,8 @@ + + diff --git a/src/NServiceBus.Core.Tests/Encryption/ConfigureRijndaelEncryptionServiceTests.cs b/src/NServiceBus.Core.Tests/Encryption/ConfigureRijndaelEncryptionServiceTests.cs new file mode 100644 index 00000000000..2350c0009d9 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Encryption/ConfigureRijndaelEncryptionServiceTests.cs @@ -0,0 +1,140 @@ +namespace NServiceBus.Core.Tests.Encryption +{ + using System; + using System.Configuration; + using System.IO; + using System.Linq; + using NServiceBus.Config; + using NUnit.Framework; + + [TestFixture] + public class ConfigureRijndaelEncryptionServiceTests + { + + [Test] + public void Can_read_from_xml() + { + var xml = +@" + + +
+ + + + + + + +"; + + var section = ReadSectionFromText(xml); + var keys = section.ExpiredKeys.Cast() + .Select(x=>x.Key) + .ToList(); + Assert.AreEqual("key1", section.Key); + Assert.AreEqual(2,keys.Count); + Assert.Contains("key2",keys); + Assert.Contains("key3",keys); + } + + static T ReadSectionFromText(string s) where T: ConfigurationSection + { + var xml = s.Replace("'", "\""); + var tempPath = Path.GetTempFileName(); + try + { + File.WriteAllText(tempPath, xml); + + var fileMap = new ExeConfigurationFileMap + { + ExeConfigFilename = tempPath + }; + + var configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None); + return (T) configuration.GetSection(typeof(T).Name); + } + finally + { + if (File.Exists(tempPath)) + { + File.Delete(tempPath); + } + } + } + + [Test] + public void Should_throw_for_whitespace_keys_in_config() + { + var config = new RijndaelEncryptionServiceConfig + { + ExpiredKeys = new RijndaelExpiredKeyCollection + { + new RijndaelExpiredKey + { + Key = " " + } + } + }; + var exception = Assert.Throws(() => ConfigureRijndaelEncryptionService.ExtractExpiredKeysFromConfigSection(config)); + Assert.AreEqual("The RijndaelEncryptionServiceConfig has a 'ExpiredKeys' property defined however some keys have no data.", exception.Message); + } + + [Test] + public void Should_throw_for_null_keys_in_config() + { + var config = new RijndaelEncryptionServiceConfig + { + ExpiredKeys = new RijndaelExpiredKeyCollection + { + new RijndaelExpiredKey() + } + }; + var exception = Assert.Throws(() => ConfigureRijndaelEncryptionService.ExtractExpiredKeysFromConfigSection(config)); + Assert.AreEqual("The RijndaelEncryptionServiceConfig has a 'ExpiredKeys' property defined however some keys have no data.", exception.Message); + } + + [Test] + public void Should_for_duplicate_between_key_and_keys_in_config() + { + var config = new RijndaelEncryptionServiceConfig + { + Key = "a", + ExpiredKeys = new RijndaelExpiredKeyCollection + { + new RijndaelExpiredKey + { + Key = "a" + } + } + }; + var exception = Assert.Throws(() => ConfigureRijndaelEncryptionService.ExtractExpiredKeysFromConfigSection(config)); + Assert.AreEqual("The RijndaelEncryptionServiceConfig has a 'Key' that is also defined inside the 'ExpiredKeys'.", exception.Message); + } + + [Test] + public void Duplicates_should_be_skipped() + { + var config = new RijndaelEncryptionServiceConfig + { + ExpiredKeys = new RijndaelExpiredKeyCollection + { + new RijndaelExpiredKey + { + Key = "a" + }, + new RijndaelExpiredKey + { + Key = "a" + } + } + }; + var keys = ConfigureRijndaelEncryptionService.ExtractExpiredKeysFromConfigSection(config); + + Assert.That(new[]{"a"}, Is.EquivalentTo(keys)); + } + } + +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs b/src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs new file mode 100644 index 00000000000..d51435ac4c0 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs @@ -0,0 +1,81 @@ +namespace NServiceBus.Core.Tests.Encryption +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Security.Cryptography; + using System.Text; + using NServiceBus.Encryption; + using NServiceBus.Encryption.Rijndael; + using NUnit.Framework; + + [TestFixture] + public class EncryptionServiceTests + { + [Test] + public void Should_encrypt_and_decrypt() + { + var service = (IEncryptionService)new EncryptionService + { + Key = Encoding.ASCII.GetBytes("gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6") + }; + var encryptedValue = service.Encrypt("string to encrypt"); + Assert.AreNotEqual("string to encrypt", encryptedValue.EncryptedBase64Value); + var decryptedValue = service.Decrypt(encryptedValue); + Assert.AreEqual("string to encrypt", decryptedValue); + } + + [Test] + public void Should_encrypt_and_decrypt_for_expired_key() + { + var service1 = (IEncryptionService)new EncryptionService + { + Key = Encoding.ASCII.GetBytes("gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"), + }; + var encryptedValue = service1.Encrypt("string to encrypt"); + Assert.AreNotEqual("string to encrypt", encryptedValue.EncryptedBase64Value); + + + var service2 = (IEncryptionService)new EncryptionService + { + Key = Encoding.ASCII.GetBytes("adDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"), + ExpiredKeys = new List + { + Encoding.ASCII.GetBytes("gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6") + } + }; + + var decryptedValue = service2.Decrypt(encryptedValue); + Assert.AreEqual("string to encrypt", decryptedValue); + } + + [Test] + public void Should_throw_when_decrypt_with_wrong_key() + { + var service1 = (IEncryptionService)new EncryptionService + { + Key = Encoding.ASCII.GetBytes("gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"), + }; + var encryptedValue = service1.Encrypt("string to encrypt"); + Assert.AreNotEqual("string to encrypt", encryptedValue.EncryptedBase64Value); + + var invalidKey = "adDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"; + var invalidExpiredKeys = new List { "bdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6" }; + + var service2 = (IEncryptionService)new EncryptionService + { + Key = Encoding.ASCII.GetBytes(invalidKey), + ExpiredKeys = invalidExpiredKeys.Select(s => Encoding.ASCII.GetBytes(s)).ToList() + }; + + var exception = Assert.Throws(() => service2.Decrypt(encryptedValue)); + Assert.AreEqual("Could not decrypt message. Tried 2 keys.", exception.Message); + Assert.AreEqual(2, exception.InnerExceptions.Count); + foreach (var inner in exception.InnerExceptions) + { + Assert.IsInstanceOf(inner); + } + } + + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj index ca8306809c8..625bd7f6920 100644 --- a/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj +++ b/src/NServiceBus.Core.Tests/NServiceBus.Core.Tests.csproj @@ -136,11 +136,13 @@ + + diff --git a/src/NServiceBus.Core/Config/RijndaelEncryptionServiceConfig.cs b/src/NServiceBus.Core/Config/RijndaelEncryptionServiceConfig.cs index ace203d982e..fcf67ecd3e7 100644 --- a/src/NServiceBus.Core/Config/RijndaelEncryptionServiceConfig.cs +++ b/src/NServiceBus.Core/Config/RijndaelEncryptionServiceConfig.cs @@ -19,5 +19,20 @@ public string Key this["Key"] = value; } } + /// + /// Contains the expired decryptions that are currently being phased out. + /// + [ConfigurationProperty("ExpiredKeys", IsRequired = false)] + public RijndaelExpiredKeyCollection ExpiredKeys + { + get + { + return this["ExpiredKeys"] as RijndaelExpiredKeyCollection; + } + set + { + this["ExpiredKeys"] = value; + } + } } } diff --git a/src/NServiceBus.Core/Config/RijndaelExpiredKey.cs b/src/NServiceBus.Core/Config/RijndaelExpiredKey.cs new file mode 100644 index 00000000000..1274b1029b8 --- /dev/null +++ b/src/NServiceBus.Core/Config/RijndaelExpiredKey.cs @@ -0,0 +1,35 @@ +namespace NServiceBus.Config +{ + using System; + using System.Configuration; + + /// + /// A configuration element representing a Rijndael encryption key. + /// + public class RijndaelExpiredKey : ConfigurationElement, IComparable + { + + /// + /// The keys value. + /// + [ConfigurationProperty("Key", IsRequired = true)] + public string Key + { + get + { + return (string)this["Key"]; + } + set + { + this["Key"] = value; + } + } + + + int IComparable.CompareTo(RijndaelExpiredKey other) + { + return String.Compare(Key, other.Key, StringComparison.Ordinal); + } + + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Config/RijndaelExpiredKeyCollection.cs b/src/NServiceBus.Core/Config/RijndaelExpiredKeyCollection.cs new file mode 100644 index 00000000000..352b1eb788e --- /dev/null +++ b/src/NServiceBus.Core/Config/RijndaelExpiredKeyCollection.cs @@ -0,0 +1,148 @@ +namespace NServiceBus.Config +{ + using System; + using System.Configuration; + + /// + /// A configuration element collection of s. + /// + public class RijndaelExpiredKeyCollection : ConfigurationElementCollection + { + /// + /// Returns AddRemoveClearMap. + /// + public override ConfigurationElementCollectionType CollectionType + { + get + { + return ConfigurationElementCollectionType.AddRemoveClearMap; + } + } + + /// + /// Creates a new . + /// + protected override ConfigurationElement CreateNewElement() + { + return new RijndaelExpiredKey(); + } + + /// + /// Creates a new , setting its property to the given value. + /// + protected override ConfigurationElement CreateNewElement(string elementName) + { + return new RijndaelExpiredKey + { + Key = elementName + }; + } + + /// + /// Returns the Messages property of the given element. + /// + protected override Object GetElementKey(ConfigurationElement element) + { + var encryptionKey = (RijndaelExpiredKey)element; + + return encryptionKey.Key; + } + + /// + /// Gets/sets the at the given index. + /// + public RijndaelExpiredKey this[int index] + { + get + { + return (RijndaelExpiredKey)BaseGet(index); + } + set + { + if (BaseGet(index) != null) + { + BaseRemoveAt(index); + } + BaseAdd(index, value); + } + } + + /// + /// Gets the for the given key. + /// + new public RijndaelExpiredKey this[string key] + { + get + { + return (RijndaelExpiredKey)BaseGet(key); + } + } + + /// + /// Calls BaseIndexOf on the given . + /// + public int IndexOf(RijndaelExpiredKey encryptionKey) + { + return BaseIndexOf(encryptionKey); + } + + /// + /// Calls BaseAdd. + /// + public void Add(RijndaelExpiredKey encryptionKey) + { + BaseAdd(encryptionKey); + } + + /// + /// Calls BaseAdd with true as the additional parameter. + /// + protected override void BaseAdd(ConfigurationElement element) + { + BaseAdd(element, true); + } + + /// + /// If the key exists, calls BaseRemove on it. + /// + public void Remove(RijndaelExpiredKey encryptionKey) + { + if (BaseIndexOf(encryptionKey) >= 0) + { + BaseRemove(encryptionKey.Key); + } + } + + /// + /// Calls BaseRemoveAt. + /// + public void RemoveAt(int index) + { + BaseRemoveAt(index); + } + + /// + /// Calls BaseRemove. + /// + public void Remove(string name) + { + BaseRemove(name); + } + + /// + /// Calls BaseClear. + /// + public void Clear() + { + BaseClear(); + } + + /// + /// True if the collection is readonly + /// + public override bool IsReadOnly() + { + return false; + } + } +} \ No newline at end of file diff --git a/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs b/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs index 6ba9beb7477..0a76762726a 100644 --- a/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs +++ b/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs @@ -1,5 +1,8 @@ namespace NServiceBus { + using System; + using System.Collections.Generic; + using System.Linq; using System.Text; using Config; using Encryption.Rijndael; @@ -25,11 +28,44 @@ public static Configure RijndaelEncryptionService(this Configure config) var encryptConfig = config.Configurer.ConfigureComponent(DependencyLifecycle.SingleInstance); if (section != null) + { + if (string.IsNullOrWhiteSpace(section.Key)) + { + throw new Exception("The RijndaelEncryptionServiceConfig has an empty 'Key' attribute."); + } + var expiredKeys = ExtractExpiredKeysFromConfigSection(section); encryptConfig.ConfigureProperty(s => s.Key, Encoding.ASCII.GetBytes(section.Key)); + encryptConfig.ConfigureProperty(s => s.ExpiredKeys, expiredKeys.Select(x=>Encoding.ASCII.GetBytes(x)).ToList()); + } return config; } + internal static List ExtractExpiredKeysFromConfigSection(RijndaelEncryptionServiceConfig section) + { + if (section.ExpiredKeys == null) + { + return new List(); + } + var encryptionKeys = section.ExpiredKeys + .Cast() + .Select(x => x.Key) + .ToList(); + if (encryptionKeys.Any(string.IsNullOrWhiteSpace)) + { + throw new Exception("The RijndaelEncryptionServiceConfig has a 'ExpiredKeys' property defined however some keys have no data."); + } + if (encryptionKeys.Any(x => x == section.Key)) + { + throw new Exception("The RijndaelEncryptionServiceConfig has a 'Key' that is also defined inside the 'ExpiredKeys'."); + } + + if (encryptionKeys.Count != encryptionKeys.Distinct().Count()) + { + throw new Exception("The RijndaelEncryptionServiceConfig has overlapping ExpiredKeys defined. Please ensure that no keys overlap in the 'ExpiredKeys' property."); + } + return encryptionKeys; + } private static readonly ILog Logger = LogManager.GetLogger(typeof(RijndaelEncryptionServiceConfig)); } } diff --git a/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs b/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs index 6059f98269a..78e6b89d0ed 100644 --- a/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs +++ b/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs @@ -1,6 +1,7 @@ namespace NServiceBus.Encryption.Rijndael { using System; + using System.Collections.Generic; using System.IO; using System.Security.Cryptography; using Logging; @@ -17,6 +18,11 @@ public class EncryptionService : IEncryptionService /// public byte[] Key { get; set; } + /// + /// Expired keys that are being phased out but still used for decryption + /// + public List ExpiredKeys { get; set; } + string IEncryptionService.Decrypt(EncryptedValue encryptedValue) { if (Key == null) @@ -25,21 +31,43 @@ string IEncryptionService.Decrypt(EncryptedValue encryptedValue) return encryptedValue.EncryptedBase64Value; } + var decryptionKeys = new List{Key}; + if (ExpiredKeys != null) + { + decryptionKeys.AddRange(ExpiredKeys); + } var encrypted = Convert.FromBase64String(encryptedValue.EncryptedBase64Value); + var cryptographicExceptions = new List(); using (var rijndael = new RijndaelManaged()) { - rijndael.Key = Key; rijndael.IV = Convert.FromBase64String(encryptedValue.Base64Iv); rijndael.Mode = CipherMode.CBC; - using (var decryptor = rijndael.CreateDecryptor()) - using (var memoryStream = new MemoryStream(encrypted)) - using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) - using (var reader = new StreamReader(cryptoStream)) + foreach (var key in decryptionKeys) { - return reader.ReadToEnd(); + rijndael.Key = key; + try + { + return Decrypt(rijndael, encrypted); + } + catch (CryptographicException exception) + { + cryptographicExceptions.Add(exception); + } } } + var message = string.Format("Could not decrypt message. Tried {0} keys.", decryptionKeys.Count); + throw new AggregateException(message, cryptographicExceptions); + } + static string Decrypt(RijndaelManaged rijndael, byte[] encrypted) + { + using (var decryptor = rijndael.CreateDecryptor()) + using (var memoryStream = new MemoryStream(encrypted)) + using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) + using (var reader = new StreamReader(cryptoStream)) + { + return reader.ReadToEnd(); + } } EncryptedValue IEncryptionService.Encrypt(string value) diff --git a/src/NServiceBus.Core/NServiceBus.Core.csproj b/src/NServiceBus.Core/NServiceBus.Core.csproj index 4cea400476f..562bab321d2 100644 --- a/src/NServiceBus.Core/NServiceBus.Core.csproj +++ b/src/NServiceBus.Core/NServiceBus.Core.csproj @@ -103,6 +103,8 @@ + + From bef9609b0952ce91f1bd848853237d89a19f35eb Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Tue, 19 Aug 2014 09:20:35 +1000 Subject: [PATCH 2/2] VerifyKeysAreNotTooSimilar --- .../Encryption/EncryptionServiceTests.cs | 13 ++++ .../ConfigureRijndaelEncryptionService.cs | 14 ++-- .../Encryption/Rijndael/EncryptionService.cs | 72 ++++++++++++------- 3 files changed, 71 insertions(+), 28 deletions(-) diff --git a/src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs b/src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs index d51435ac4c0..f9648b250d2 100644 --- a/src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs +++ b/src/NServiceBus.Core.Tests/Encryption/EncryptionServiceTests.cs @@ -77,5 +77,18 @@ public void Should_throw_when_decrypt_with_wrong_key() } } + [Test] + public void Should_throw_when_encrypt_and_decrypt_keys_are_too_similar() + { + var key = Encoding.ASCII.GetBytes("gdDbqRpqdRbTs3mhdZh9qCaDaxJXl+e6"); + var service = new EncryptionService + { + Key = key, + ExpiredKeys = new List { key } //note that we use the same key to get the code to throw + }; + var exception = Assert.Throws(service.VerifyKeysAreNotTooSimilar); + + Assert.AreEqual("The new Encryption Key is too similar to the Expired Key at index 0. This can cause issues when decrypting data. To fix this issue please ensure the new encryption key is not too similar to the existing Expired Keys.", exception.Message); + } } } \ No newline at end of file diff --git a/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs b/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs index 0a76762726a..bcfb0bd6884 100644 --- a/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs +++ b/src/NServiceBus.Core/ConfigureRijndaelEncryptionService.cs @@ -5,6 +5,7 @@ namespace NServiceBus using System.Linq; using System.Text; using Config; + using Encryption; using Encryption.Rijndael; using Logging; @@ -25,7 +26,7 @@ public static Configure RijndaelEncryptionService(this Configure config) if (section == null) Logger.Warn("Could not find configuration section for Rijndael Encryption Service."); - var encryptConfig = config.Configurer.ConfigureComponent(DependencyLifecycle.SingleInstance); + var encryptionService = new EncryptionService(); if (section != null) { @@ -34,10 +35,15 @@ public static Configure RijndaelEncryptionService(this Configure config) throw new Exception("The RijndaelEncryptionServiceConfig has an empty 'Key' attribute."); } var expiredKeys = ExtractExpiredKeysFromConfigSection(section); - encryptConfig.ConfigureProperty(s => s.Key, Encoding.ASCII.GetBytes(section.Key)); - encryptConfig.ConfigureProperty(s => s.ExpiredKeys, expiredKeys.Select(x=>Encoding.ASCII.GetBytes(x)).ToList()); + + encryptionService.Key = Encoding.ASCII.GetBytes(section.Key); + encryptionService.ExpiredKeys = expiredKeys.Select(x => Encoding.ASCII.GetBytes(x)).ToList(); } + encryptionService.VerifyKeysAreNotTooSimilar(); + + config.Configurer.RegisterSingleton(encryptionService); + return config; } @@ -68,4 +74,4 @@ internal static List ExtractExpiredKeysFromConfigSection(RijndaelEncrypt } private static readonly ILog Logger = LogManager.GetLogger(typeof(RijndaelEncryptionServiceConfig)); } -} +} \ No newline at end of file diff --git a/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs b/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs index 78e6b89d0ed..9d12dbcf32e 100644 --- a/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs +++ b/src/NServiceBus.Core/Encryption/Rijndael/EncryptionService.cs @@ -31,42 +31,43 @@ string IEncryptionService.Decrypt(EncryptedValue encryptedValue) return encryptedValue.EncryptedBase64Value; } - var decryptionKeys = new List{Key}; + var decryptionKeys = new List { Key }; if (ExpiredKeys != null) { decryptionKeys.AddRange(ExpiredKeys); } - var encrypted = Convert.FromBase64String(encryptedValue.EncryptedBase64Value); var cryptographicExceptions = new List(); - using (var rijndael = new RijndaelManaged()) - { - rijndael.IV = Convert.FromBase64String(encryptedValue.Base64Iv); - rijndael.Mode = CipherMode.CBC; - foreach (var key in decryptionKeys) + foreach (var key in decryptionKeys) + { + try { - rijndael.Key = key; - try - { - return Decrypt(rijndael, encrypted); - } - catch (CryptographicException exception) - { - cryptographicExceptions.Add(exception); - } + return Decrypt(encryptedValue, key); + } + catch (CryptographicException exception) + { + cryptographicExceptions.Add(exception); } } var message = string.Format("Could not decrypt message. Tried {0} keys.", decryptionKeys.Count); throw new AggregateException(message, cryptographicExceptions); } - static string Decrypt(RijndaelManaged rijndael, byte[] encrypted) + + static string Decrypt(EncryptedValue encryptedValue, byte[] key) { - using (var decryptor = rijndael.CreateDecryptor()) - using (var memoryStream = new MemoryStream(encrypted)) - using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) - using (var reader = new StreamReader(cryptoStream)) + using (var rijndael = new RijndaelManaged()) { - return reader.ReadToEnd(); + var encrypted = Convert.FromBase64String(encryptedValue.EncryptedBase64Value); + rijndael.IV = Convert.FromBase64String(encryptedValue.Base64Iv); + rijndael.Mode = CipherMode.CBC; + rijndael.Key = key; + using (var decryptor = rijndael.CreateDecryptor()) + using (var memoryStream = new MemoryStream(encrypted)) + using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) + using (var reader = new StreamReader(cryptoStream)) + { + return reader.ReadToEnd(); + } } } @@ -99,6 +100,29 @@ EncryptedValue IEncryptionService.Encrypt(string value) } } - private static readonly ILog Logger = LogManager.GetLogger(typeof (EncryptionService)); + internal void VerifyKeysAreNotTooSimilar() + { + for (var index = 0; index < ExpiredKeys.Count; index++) + { + var decryption = ExpiredKeys[index]; + CryptographicException exception = null; + var encryptedValue = ((IEncryptionService)this).Encrypt("a"); + try + { + Decrypt(encryptedValue, decryption); + } + catch (CryptographicException cryptographicException) + { + exception = cryptographicException; + } + if (exception == null) + { + var message = string.Format("The new Encryption Key is too similar to the Expired Key at index {0}. This can cause issues when decrypting data. To fix this issue please ensure the new encryption key is not too similar to the existing Expired Keys.", index); + throw new Exception(message); + } + } + } + + static readonly ILog Logger = LogManager.GetLogger(typeof(EncryptionService)); } -} +} \ No newline at end of file