From 2a59f1277a353be30ec06adffad7c557c0cb881e Mon Sep 17 00:00:00 2001 From: James Newton-King Date: Sat, 13 Aug 2022 09:33:56 +0800 Subject: [PATCH] Validate DataProtection custom algorithm has a constructor --- .../AuthenticatedEncryptorFactory.cs | 1 + .../ManagedAlgorithmHelpers.cs | 66 +++++++++++++++++++ ...agedAuthenticatedEncryptorConfiguration.cs | 3 + ...ManagedAuthenticatedEncryptorDescriptor.cs | 34 +--------- ...nticatedEncryptorDescriptorDeserializer.cs | 34 +--------- .../src/RegistryPolicyResolver.cs | 4 +- ...tedEncryptorDescriptorDeserializerTests.cs | 65 +++++++++++++++++- .../test/RegistryPolicyResolverTests.cs | 4 +- 8 files changed, 142 insertions(+), 69 deletions(-) create mode 100644 src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAlgorithmHelpers.cs diff --git a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs index e9b45de7f1f2..15351ac69936 100644 --- a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs +++ b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/AuthenticatedEncryptorFactory.cs @@ -163,6 +163,7 @@ private static string GetBCryptAlgorithmNameFromValidationAlgorithm(ValidationAl } } + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] private static Type GetManagedTypeFromEncryptionAlgorithm(EncryptionAlgorithm algorithm) { switch (algorithm) diff --git a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAlgorithmHelpers.cs b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAlgorithmHelpers.cs new file mode 100644 index 000000000000..c9a7c3244445 --- /dev/null +++ b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAlgorithmHelpers.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; +using System.Xml.Linq; + +namespace Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel; + +internal static class ManagedAlgorithmHelpers +{ + private static readonly List KnownAlgorithmTypes = new List + { + typeof(Aes), + typeof(HMACSHA1), + typeof(HMACSHA256), + typeof(HMACSHA384), + typeof(HMACSHA512) + }; + + // Any changes to this method should also be be reflected in FriendlyNameToType. + public static string TypeToFriendlyName(Type type) + { + if (KnownAlgorithmTypes.Contains(type)) + { + return type.Name; + } + else + { + return type.AssemblyQualifiedName!; + } + } + + // Any changes to this method should also be be reflected in TypeToFriendlyName. + [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] + [UnconditionalSuppressMessage("Trimmer", "IL2075", Justification = "Unknown type is checked for whether it has a public parameterless constructor. Handle trimmed types by providing a useful error message.")] + [UnconditionalSuppressMessage("Trimmer", "IL2073", Justification = "Unknown type is checked for whether it has a public parameterless constructor. Handle trimmed types by providing a useful error message.")] + public static Type FriendlyNameToType(string typeName) + { + foreach (var knownType in KnownAlgorithmTypes) + { + if (knownType.Name == typeName) + { + return knownType; + } + } + + var type = TypeExtensions.GetTypeWithTrimFriendlyErrorMessage(typeName); + + // Type name could be full or assembly qualified name of known type. + if (KnownAlgorithmTypes.Contains(type)) + { + return type; + } + + // All other types are created using Activator.CreateInstance. Validate it has a valid constructor. + if (type.GetConstructor(Type.EmptyTypes) == null) + { + throw new InvalidOperationException($"Algorithm type {type} doesn't have a public parameterless constructor. If the app is published with trimming then the constructor may have been trimmed. Ensure the type's assembly is excluded from trimming."); + } + + return type; + } +} diff --git a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs index 390c0ec4fe52..8a9262c666ff 100644 --- a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs +++ b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorConfiguration.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using Microsoft.Extensions.Logging.Abstractions; @@ -24,6 +25,7 @@ public sealed class ManagedAuthenticatedEncryptorConfiguration : AlgorithmConfig /// The default algorithm is AES. /// [ApplyPolicy] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public Type EncryptionAlgorithmType { get; set; } = typeof(Aes); /// @@ -47,6 +49,7 @@ public sealed class ManagedAuthenticatedEncryptorConfiguration : AlgorithmConfig /// The default algorithm is HMACSHA256. /// [ApplyPolicy] + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] public Type ValidationAlgorithmType { get; set; } = typeof(HMACSHA256); /// diff --git a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs index 30bafe6564a8..33816cf24867 100644 --- a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs +++ b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptor.cs @@ -49,11 +49,11 @@ public XmlSerializedDescriptorInfo ExportToXml() // var encryptionElement = new XElement("encryption", - new XAttribute("algorithm", TypeToFriendlyName(Configuration.EncryptionAlgorithmType)), + new XAttribute("algorithm", ManagedAlgorithmHelpers.TypeToFriendlyName(Configuration.EncryptionAlgorithmType)), new XAttribute("keyLength", Configuration.EncryptionAlgorithmKeySize)); var validationElement = new XElement("validation", - new XAttribute("algorithm", TypeToFriendlyName(Configuration.ValidationAlgorithmType))); + new XAttribute("algorithm", ManagedAlgorithmHelpers.TypeToFriendlyName(Configuration.ValidationAlgorithmType))); var rootElement = new XElement("descriptor", new XComment(" Algorithms provided by specified SymmetricAlgorithm and KeyedHashAlgorithm "), @@ -63,34 +63,4 @@ public XmlSerializedDescriptorInfo ExportToXml() return new XmlSerializedDescriptorInfo(rootElement, typeof(ManagedAuthenticatedEncryptorDescriptorDeserializer)); } - - // Any changes to this method should also be be reflected - // in ManagedAuthenticatedEncryptorDescriptorDeserializer.FriendlyNameToType. - private static string TypeToFriendlyName(Type type) - { - if (type == typeof(Aes)) - { - return nameof(Aes); - } - else if (type == typeof(HMACSHA1)) - { - return nameof(HMACSHA1); - } - else if (type == typeof(HMACSHA256)) - { - return nameof(HMACSHA256); - } - else if (type == typeof(HMACSHA384)) - { - return nameof(HMACSHA384); - } - else if (type == typeof(HMACSHA512)) - { - return nameof(HMACSHA512); - } - else - { - return type.AssemblyQualifiedName!; - } - } } diff --git a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs index 09fdc3aafcf4..504b7bb3ad9b 100644 --- a/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs +++ b/src/DataProtection/DataProtection/src/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializer.cs @@ -34,44 +34,14 @@ public IAuthenticatedEncryptorDescriptor ImportFromXml(XElement element) var configuration = new ManagedAuthenticatedEncryptorConfiguration(); var encryptionElement = element.Element("encryption")!; - configuration.EncryptionAlgorithmType = FriendlyNameToType((string)encryptionElement.Attribute("algorithm")!); + configuration.EncryptionAlgorithmType = ManagedAlgorithmHelpers.FriendlyNameToType((string)encryptionElement.Attribute("algorithm")!); configuration.EncryptionAlgorithmKeySize = (int)encryptionElement.Attribute("keyLength")!; var validationElement = element.Element("validation")!; - configuration.ValidationAlgorithmType = FriendlyNameToType((string)validationElement.Attribute("algorithm")!); + configuration.ValidationAlgorithmType = ManagedAlgorithmHelpers.FriendlyNameToType((string)validationElement.Attribute("algorithm")!); Secret masterKey = ((string)element.Element("masterKey")!).ToSecret(); return new ManagedAuthenticatedEncryptorDescriptor(configuration, masterKey); } - - // Any changes to this method should also be be reflected - // in ManagedAuthenticatedEncryptorDescriptor.TypeToFriendlyName. - private static Type FriendlyNameToType(string typeName) - { - if (typeName == nameof(Aes)) - { - return typeof(Aes); - } - else if (typeName == nameof(HMACSHA1)) - { - return typeof(HMACSHA1); - } - else if (typeName == nameof(HMACSHA256)) - { - return typeof(HMACSHA256); - } - else if (typeName == nameof(HMACSHA384)) - { - return typeof(HMACSHA384); - } - else if (typeName == nameof(HMACSHA512)) - { - return typeof(HMACSHA512); - } - else - { - return TypeExtensions.GetTypeWithTrimFriendlyErrorMessage(typeName); - } - } } diff --git a/src/DataProtection/DataProtection/src/RegistryPolicyResolver.cs b/src/DataProtection/DataProtection/src/RegistryPolicyResolver.cs index 62e81417c5ed..dd9a213acff8 100644 --- a/src/DataProtection/DataProtection/src/RegistryPolicyResolver.cs +++ b/src/DataProtection/DataProtection/src/RegistryPolicyResolver.cs @@ -177,7 +177,7 @@ private static ManagedAuthenticatedEncryptorConfiguration GetManagedAuthenticate var valueFromRegistry = key.GetValue(nameof(ManagedAuthenticatedEncryptorConfiguration.EncryptionAlgorithmType)); if (valueFromRegistry != null) { - options.EncryptionAlgorithmType = TypeExtensions.GetTypeWithTrimFriendlyErrorMessage(Convert.ToString(valueFromRegistry, CultureInfo.InvariantCulture)!); + options.EncryptionAlgorithmType = ManagedAlgorithmHelpers.FriendlyNameToType(Convert.ToString(valueFromRegistry, CultureInfo.InvariantCulture)!); } valueFromRegistry = key.GetValue(nameof(ManagedAuthenticatedEncryptorConfiguration.EncryptionAlgorithmKeySize)); @@ -189,7 +189,7 @@ private static ManagedAuthenticatedEncryptorConfiguration GetManagedAuthenticate valueFromRegistry = key.GetValue(nameof(ManagedAuthenticatedEncryptorConfiguration.ValidationAlgorithmType)); if (valueFromRegistry != null) { - options.ValidationAlgorithmType = TypeExtensions.GetTypeWithTrimFriendlyErrorMessage(Convert.ToString(valueFromRegistry, CultureInfo.InvariantCulture)!); + options.ValidationAlgorithmType = ManagedAlgorithmHelpers.FriendlyNameToType(Convert.ToString(valueFromRegistry, CultureInfo.InvariantCulture)!); } return options; diff --git a/src/DataProtection/DataProtection/test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs b/src/DataProtection/DataProtection/test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs index 3e8009677ea0..8793e806c48f 100644 --- a/src/DataProtection/DataProtection/test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs +++ b/src/DataProtection/DataProtection/test/AuthenticatedEncryption/ConfigurationModel/ManagedAuthenticatedEncryptorDescriptorDeserializerTests.cs @@ -50,7 +50,7 @@ public void ImportFromXml_BuiltInTypes_CreatesAppropriateDescriptor(Type encrypt } [Fact] - public void ImportFromXml_CustomType_CreatesAppropriateDescriptor() + public void ImportFromXml_FullyQualifiedBuiltInTypes_CreatesAppropriateDescriptor() { // Arrange var masterKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("[PLACEHOLDER]")); @@ -83,6 +83,69 @@ public void ImportFromXml_CustomType_CreatesAppropriateDescriptor() Assert.Equal(plaintext, roundTripPlaintext); } + [Fact] + public void ImportFromXml_CustomType_CreatesAppropriateDescriptor() + { + // Arrange + var masterKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("[PLACEHOLDER]")); + + var xml = $@" + + + + + {masterKey} + + "; + + // Act + var deserializedDescriptor = new ManagedAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml)); + var managedDescriptor = (ManagedAuthenticatedEncryptorDescriptor)deserializedDescriptor; + + // Assert + Assert.Equal(typeof(CustomAlgorithm), managedDescriptor.Configuration.EncryptionAlgorithmType); + } + + [Fact] + public void ImportFromXml_CustomTypeWithoutConstructor_CreatesAppropriateDescriptor() + { + // Arrange + var masterKey = Convert.ToBase64String(Encoding.UTF8.GetBytes("[PLACEHOLDER]")); + + var xml = $@" + + + + + {masterKey} + + "; + + // Act + var ex = Assert.Throws(() => new ManagedAuthenticatedEncryptorDescriptorDeserializer().ImportFromXml(XElement.Parse(xml))); + + // Assert + Assert.Equal($"Algorithm type {typeof(CustomAlgorithmNoConstructor).FullName} doesn't have a public parameterless constructor. If the app is published with trimming then the constructor may have been trimmed. Ensure the type's assembly is excluded from trimming.", ex.Message); + } + + public class CustomAlgorithm : SymmetricAlgorithm + { + public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[] rgbIV) => throw new NotImplementedException(); + public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[] rgbIV) => throw new NotImplementedException(); + public override void GenerateIV() => throw new NotImplementedException(); + public override void GenerateKey() => throw new NotImplementedException(); + } + + public class CustomAlgorithmNoConstructor : SymmetricAlgorithm + { + private CustomAlgorithmNoConstructor() { } + + public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[] rgbIV) => throw new NotImplementedException(); + public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[] rgbIV) => throw new NotImplementedException(); + public override void GenerateIV() => throw new NotImplementedException(); + public override void GenerateKey() => throw new NotImplementedException(); + } + private static IAuthenticatedEncryptor CreateEncryptorInstanceFromDescriptor(ManagedAuthenticatedEncryptorDescriptor descriptor) { var encryptorFactory = new ManagedAuthenticatedEncryptorFactory(NullLoggerFactory.Instance); diff --git a/src/DataProtection/DataProtection/test/RegistryPolicyResolverTests.cs b/src/DataProtection/DataProtection/test/RegistryPolicyResolverTests.cs index 170e3b5a5414..581e3adc6239 100644 --- a/src/DataProtection/DataProtection/test/RegistryPolicyResolverTests.cs +++ b/src/DataProtection/DataProtection/test/RegistryPolicyResolverTests.cs @@ -226,13 +226,13 @@ public void ResolvePolicy_ManagedEncryption_WithExplicitSettings() var registryEntries = new Dictionary() { ["EncryptionType"] = "managed", - ["EncryptionAlgorithmType"] = typeof(TripleDES).AssemblyQualifiedName, + ["EncryptionAlgorithmType"] = typeof(Aes).AssemblyQualifiedName, ["EncryptionAlgorithmKeySize"] = 2048, ["ValidationAlgorithmType"] = typeof(HMACSHA1).AssemblyQualifiedName }; var expectedConfiguration = new ManagedAuthenticatedEncryptorConfiguration() { - EncryptionAlgorithmType = typeof(TripleDES), + EncryptionAlgorithmType = typeof(Aes), EncryptionAlgorithmKeySize = 2048, ValidationAlgorithmType = typeof(HMACSHA1) };