From 76b76ba099ba5ad932adb9b6ef4d19bf99a61593 Mon Sep 17 00:00:00 2001 From: Levi B Date: Mon, 20 Oct 2014 14:12:04 -0700 Subject: [PATCH] DataProtectionServices should use keys stored in HKLM auto-gen registry when running on IIS without user profile. --- .../Cng/DpapiSecretSerializerHelper.cs | 4 +- .../DataProtectionServices.cs | 29 +++-- .../Repositories/RegistryXmlRepository.cs | 117 ++++++++++++++++++ .../XmlEncryption/DpapiXmlEncryptor.cs | 9 +- .../project.json | 1 + 5 files changed, 150 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.AspNet.Security.DataProtection/Repositories/RegistryXmlRepository.cs diff --git a/src/Microsoft.AspNet.Security.DataProtection/Cng/DpapiSecretSerializerHelper.cs b/src/Microsoft.AspNet.Security.DataProtection/Cng/DpapiSecretSerializerHelper.cs index 6c0f368847..9f1dbb2a6e 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/Cng/DpapiSecretSerializerHelper.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/Cng/DpapiSecretSerializerHelper.cs @@ -22,7 +22,7 @@ namespace Microsoft.AspNet.Security.DataProtection.Cng private static readonly byte[] _purpose = Encoding.UTF8.GetBytes("DPAPI-Protected Secret"); - public static byte[] ProtectWithDpapi(ISecret secret) + public static byte[] ProtectWithDpapi(ISecret secret, bool protectToLocalMachine = false) { Debug.Assert(secret != null); @@ -34,7 +34,7 @@ namespace Microsoft.AspNet.Security.DataProtection.Cng secret.WriteSecretIntoBuffer(new ArraySegment(plaintextSecret)); fixed (byte* pbPurpose = _purpose) { - return ProtectWithDpapiImpl(pbPlaintextSecret, (uint)plaintextSecret.Length, pbPurpose, (uint)_purpose.Length); + return ProtectWithDpapiImpl(pbPlaintextSecret, (uint)plaintextSecret.Length, pbPurpose, (uint)_purpose.Length, fLocalMachine: protectToLocalMachine); } } finally diff --git a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionServices.cs b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionServices.cs index f24ae68036..cc3d2d7731 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/DataProtectionServices.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/DataProtectionServices.cs @@ -72,18 +72,33 @@ namespace Microsoft.AspNet.Security.DataProtection { descriptors.AddRange(new[] { - describe.Singleton(), + describe.Instance(new DpapiXmlEncryptor(protectToLocalMachine: false)), describe.Instance(new FileSystemXmlRepository(localAppDataKeysFolder)) }); } else { - // Are we running with no user profile (e.g., IIS service)? - // Fall back to DPAPI for now. - // TODO: We should use the IIS auto-gen reg keys as our repository. - return new[] { - describe.Instance(new DpapiDataProtectionProvider(DataProtectionScope.LocalMachine)) - }; + // If we've reached this point, we have no user profile loaded. + + RegistryXmlRepository hklmRegXmlRepository = RegistryXmlRepository.GetDefaultRepositoryForHKLMRegistry(); + if (hklmRegXmlRepository != null) + { + // Have WAS and IIS created an auto-gen key folder in the HKLM registry for us? + // If so, use it as the repository, and use DPAPI as the key protection mechanism. + // We use same-machine DPAPI since we already know no user profile is loaded. + descriptors.AddRange(new[] + { + describe.Instance(new DpapiXmlEncryptor(protectToLocalMachine: true)), + describe.Instance(hklmRegXmlRepository) + }); + } + else + { + // Fall back to DPAPI for now + return new[] { + describe.Instance(new DpapiDataProtectionProvider(DataProtectionScope.LocalMachine)) + }; + } } } diff --git a/src/Microsoft.AspNet.Security.DataProtection/Repositories/RegistryXmlRepository.cs b/src/Microsoft.AspNet.Security.DataProtection/Repositories/RegistryXmlRepository.cs new file mode 100644 index 0000000000..6ebbed369b --- /dev/null +++ b/src/Microsoft.AspNet.Security.DataProtection/Repositories/RegistryXmlRepository.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Xml.Linq; +using Microsoft.Win32; + +namespace Microsoft.AspNet.Security.DataProtection.Repositories +{ + /// + /// An XML repository backed by the Windows registry. + /// + public class RegistryXmlRepository : IXmlRepository + { + public RegistryXmlRepository([NotNull] RegistryKey registryKey) + { + RegistryKey = registryKey; + } + + protected RegistryKey RegistryKey + { + get; + private set; + } + + public virtual IReadOnlyCollection GetAllElements() + { + // forces complete enumeration + return GetAllElementsImpl().ToArray(); + } + + private IEnumerable GetAllElementsImpl() + { + string[] allValueNames = RegistryKey.GetValueNames(); + foreach (var valueName in allValueNames) + { + string thisValue = RegistryKey.GetValue(valueName) as string; + if (!String.IsNullOrEmpty(thisValue)) + { + XDocument document; + using (var textReader = new StringReader(thisValue)) + { + document = XDocument.Load(textReader); + } + + // 'yield return' outside the preceding 'using' block so we can release the reader + yield return document.Root; + } + } + } + + internal static RegistryXmlRepository GetDefaultRepositoryForHKLMRegistry() + { + try + { + // Try reading the auto-generated machine key from HKLM + using (var hklmBaseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32)) + { + // TODO: Do we need to change the version number below? + string aspnetAutoGenKeysBaseKeyName = String.Format(CultureInfo.InvariantCulture, @"SOFTWARE\Microsoft\ASP.NET\4.0.30319.0\AutoGenKeys\{0}", WindowsIdentity.GetCurrent().User.Value); + var aspnetBaseKey = hklmBaseKey.OpenSubKey(aspnetAutoGenKeysBaseKeyName, writable: true); + if (aspnetBaseKey == null) + { + return null; // couldn't find the auto-generated machine key + } + + using (aspnetBaseKey) { + // TODO: Remove the ".BETA" moniker. + var dataProtectionKey = aspnetBaseKey.OpenSubKey("DataProtection.BETA", writable: true); + if (dataProtectionKey == null) + { + // TODO: Remove the ".BETA" moniker from here, also. + dataProtectionKey = aspnetBaseKey.CreateSubKey("DataProtection.BETA"); + } + + // Once we've opened the HKLM reg key, return a repository which wraps it. + return new RegistryXmlRepository(dataProtectionKey); + } + } + } + catch + { + // swallow all errors; they're not fatal + return null; + } + } + + public virtual void StoreElement([NotNull] XElement element, string friendlyName) + { + // We're going to ignore the friendly name for now and just use a GUID. + StoreElement(element, Guid.NewGuid()); + } + + private void StoreElement(XElement element, Guid id) + { + // First, serialize the XElement to a string. + string serializedString; + using (var writer = new StringWriter()) + { + new XDocument(element).Save(writer); + serializedString = writer.ToString(); + } + + // Technically calls to RegSetValue* and RegGetValue* are atomic, so we don't have to worry about + // another thread trying to read this value while we're writing it. There's still a small risk of + // data corruption if power is lost while the registry file is being flushed to the file system, + // but the window for that should be small enough that we shouldn't have to worry about it. + string idAsString = id.ToString("D"); + RegistryKey.SetValue(idAsString, serializedString, RegistryValueKind.String); + } + } +} diff --git a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs index 718758673f..6f33ed7ebf 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs +++ b/src/Microsoft.AspNet.Security.DataProtection/XmlEncryption/DpapiXmlEncryptor.cs @@ -16,6 +16,13 @@ namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption { internal static readonly XName DpapiEncryptedSecretElementName = XmlKeyManager.KeyManagementXmlNamespace.GetName("dpapiEncryptedSecret"); + private readonly bool _protectToLocalMachine; + + public DpapiXmlEncryptor(bool protectToLocalMachine) + { + _protectToLocalMachine = protectToLocalMachine; + } + /// /// Encrypts the specified XML element using Windows DPAPI. /// @@ -45,7 +52,7 @@ namespace Microsoft.AspNet.Security.DataProtection.XmlEncryption // // ... base64 data ... // - byte[] encryptedBytes = DpapiSecretSerializerHelper.ProtectWithDpapi(secret); + byte[] encryptedBytes = DpapiSecretSerializerHelper.ProtectWithDpapi(secret, protectToLocalMachine: _protectToLocalMachine); return new XElement(DpapiEncryptedSecretElementName, new XAttribute("decryptor", typeof(DpapiXmlDecryptor).AssemblyQualifiedName), new XAttribute("version", 1), diff --git a/src/Microsoft.AspNet.Security.DataProtection/project.json b/src/Microsoft.AspNet.Security.DataProtection/project.json index a6382d436f..51b85a98a0 100644 --- a/src/Microsoft.AspNet.Security.DataProtection/project.json +++ b/src/Microsoft.AspNet.Security.DataProtection/project.json @@ -27,6 +27,7 @@ "dependencies": { "Microsoft.Framework.DependencyInjection": "1.0.0-*", "Microsoft.Framework.OptionsModel": "1.0.0-*", + "Microsoft.Win32.Registry": "4.0.0-beta-*", "System.Diagnostics.Debug": "4.0.10-beta-*", "System.Diagnostics.Tools": "4.0.0-beta-*", "System.Globalization": "4.0.10-beta-*",