Skip to content

Commit 53f6dbb

Browse files
authored
Support reading existing DataProtection KeyVault keys (#14778)
1 parent 6171954 commit 53f6dbb

8 files changed

+354
-5
lines changed

sdk/extensions/Azure.Extensions.AspNetCore.DataProtection.Keys/CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
# Release History
22

3-
## 1.1.0-preview.1 (Unreleased)
3+
## 1.0.2 (2020-09-01)
44

5+
### Fixed
6+
7+
- Support reading keys created by a previous version of Azure KeyVault Keys DataProtection library.
58

69
## 1.0.1 (2020-08-06)
710

sdk/extensions/Azure.Extensions.AspNetCore.DataProtection.Keys/src/Azure.Extensions.AspNetCore.DataProtection.Keys.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<Description>Microsoft Azure Key Vault key encryption support.</Description>
55
<PackageTags>aspnetcore;dataprotection;azure;keyvault</PackageTags>
6-
<Version>1.1.0-preview.1</Version>
6+
<Version>1.0.2</Version>
77
<ApiCompatVersion>1.0.1</ApiCompatVersion>
88
<IsExtensionClientLibrary>true</IsExtensionClientLibrary>
99
<NoWarn>$(NoWarn);AZC0102</NoWarn>

sdk/extensions/Azure.Extensions.AspNetCore.DataProtection.Keys/src/AzureDataProtectionKeyVaultKeyBuilderExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using Azure.Core;
77
using Azure.Core.Cryptography;
88
using Azure.Security.KeyVault.Keys.Cryptography;
9+
using Microsoft.AspNetCore.DataProtection.Internal;
910
using Microsoft.AspNetCore.DataProtection.KeyManagement;
1011
using Microsoft.Extensions.DependencyInjection;
1112

@@ -45,6 +46,7 @@ public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProt
4546
Argument.AssertNotNullOrEmpty(keyIdentifier, nameof(keyIdentifier));
4647

4748
builder.Services.AddSingleton<IKeyEncryptionKeyResolver>(keyResolver);
49+
builder.Services.AddSingleton<IActivator, DecryptorTypeForwardingActivator>();
4850
builder.Services.Configure<KeyManagementOptions>(options =>
4951
{
5052
options.XmlEncryptor = new AzureKeyVaultXmlEncryptor(keyResolver, keyIdentifier);

sdk/extensions/Azure.Extensions.AspNetCore.DataProtection.Keys/src/AzureKeyVaultXmlEncryptor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ private async Task<EncryptedXmlInfo> EncryptAsync(XElement plaintextElement)
6666
var wrappedKey = await key.WrapKeyAsync(DefaultKeyEncryption, symmetricKey).ConfigureAwait(false);
6767

6868
var element = new XElement("encryptedKey",
69-
new XComment(" This key is encrypted with Azure KeyVault. "),
69+
new XComment(" This key is encrypted with Azure Key Vault. "),
7070
new XElement("kid", key.KeyId),
7171
new XElement("key", Convert.ToBase64String(wrappedKey)),
7272
new XElement("iv", Convert.ToBase64String(symmetricIV)),
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.Text.RegularExpressions;
6+
using Microsoft.AspNetCore.DataProtection.Internal;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Abstractions;
9+
10+
#pragma warning disable AZC0001
11+
namespace Microsoft.AspNetCore.DataProtection
12+
#pragma warning restore AZC0001
13+
{
14+
/// <summary>
15+
/// This type is a copy of https://github.com/dotnet/aspnetcore/blob/e1bf38ccaf2a98f95e48bf22b8b76d996a0c33ea/src/DataProtection/DataProtection/test/TypeForwardingActivatorTests.cs
16+
/// with addition of AzureKeyVaultXmlDecryptor forwarding logic.
17+
/// </summary>
18+
internal class DecryptorTypeForwardingActivator : IActivator
19+
{
20+
private readonly IServiceProvider _services;
21+
private const string OldNamespace = "Microsoft.AspNet.DataProtection";
22+
private const string CurrentNamespace = "Microsoft.AspNetCore.DataProtection";
23+
private const string CurrentAzureNamespace = "Azure.Extensions.AspNetCore.DataProtection.Keys";
24+
25+
private const string OldDecryptor = "Microsoft.AspNetCore.DataProtection.AzureKeyVault.AzureKeyVaultXmlDecryptor";
26+
private const string OldDecryptorAssembly = "Microsoft.AspNetCore.DataProtection.AzureKeyVault";
27+
private const string NewDecryptor = "Azure.Extensions.AspNetCore.DataProtection.Keys.AzureKeyVaultXmlDecryptor";
28+
private const string NewDecryptorAssembly = "Azure.Extensions.AspNetCore.DataProtection.Keys";
29+
30+
private readonly ILogger _logger;
31+
private static readonly Regex _versionPattern = new Regex(@",\s?Version=[0-9]+(\.[0-9]+){0,3}", RegexOptions.Compiled, TimeSpan.FromSeconds(2));
32+
private static readonly Regex _tokenPattern = new Regex(@",\s?PublicKeyToken=[\w\d]+", RegexOptions.Compiled, TimeSpan.FromSeconds(2));
33+
34+
public DecryptorTypeForwardingActivator(IServiceProvider services)
35+
: this(services, NullLoggerFactory.Instance)
36+
{
37+
}
38+
39+
public DecryptorTypeForwardingActivator(IServiceProvider services, ILoggerFactory loggerFactory)
40+
{
41+
_services = services;
42+
_logger = loggerFactory.CreateLogger(typeof(DecryptorTypeForwardingActivator));
43+
}
44+
45+
public object CreateInstance(Type expectedBaseType, string originalTypeName)
46+
=> CreateInstance(expectedBaseType, originalTypeName, out var _);
47+
48+
// for testing
49+
internal object CreateInstance(Type expectedBaseType, string originalTypeName, out bool forwarded)
50+
{
51+
var forwardedTypeName = originalTypeName;
52+
var candidate = false;
53+
if (originalTypeName.Contains(OldNamespace))
54+
{
55+
candidate = true;
56+
forwardedTypeName = originalTypeName.Replace(OldNamespace, CurrentNamespace);
57+
}
58+
59+
if (originalTypeName.Contains(OldDecryptor))
60+
{
61+
candidate = true;
62+
forwardedTypeName = originalTypeName
63+
.Replace(OldDecryptor, NewDecryptor)
64+
.Replace(OldDecryptorAssembly, NewDecryptorAssembly);
65+
}
66+
67+
if ((candidate || forwardedTypeName.StartsWith(CurrentNamespace + ".", StringComparison.Ordinal)) ||
68+
forwardedTypeName.StartsWith(CurrentAzureNamespace + ".", StringComparison.Ordinal))
69+
{
70+
candidate = true;
71+
forwardedTypeName = RemoveVersionFromAssemblyName(forwardedTypeName);
72+
forwardedTypeName = RemovePublicKeyTokenFromAssemblyName(forwardedTypeName);
73+
}
74+
75+
if (candidate)
76+
{
77+
var type = Type.GetType(forwardedTypeName, false);
78+
if (type != null)
79+
{
80+
_logger.LogDebug("Forwarded activator type request from {FromType} to {ToType}",
81+
originalTypeName,
82+
forwardedTypeName);
83+
forwarded = true;
84+
return CreateInstanceImpl(expectedBaseType, forwardedTypeName);
85+
}
86+
}
87+
88+
forwarded = false;
89+
return CreateInstanceImpl(expectedBaseType, originalTypeName);
90+
}
91+
92+
private static string RemovePublicKeyTokenFromAssemblyName(string forwardedTypeName)
93+
=> _tokenPattern.Replace(forwardedTypeName, "");
94+
95+
internal static string RemoveVersionFromAssemblyName(string forwardedTypeName)
96+
=> _versionPattern.Replace(forwardedTypeName, "");
97+
98+
private object CreateInstanceImpl(Type expectedBaseType, string implementationTypeName)
99+
{
100+
// Would the assignment even work?
101+
var implementationType = Type.GetType(implementationTypeName, throwOnError: true);
102+
103+
if (!expectedBaseType.IsAssignableFrom(implementationType))
104+
{
105+
// It might seem a bit strange to throw an InvalidCastException explicitly rather than
106+
// to let the CLR generate one, but searching through NetFX there is indeed precedent
107+
// for this pattern when the caller knows ahead of time the operation will fail.
108+
throw new InvalidCastException($"The type '{implementationType.AssemblyQualifiedName}' is not assignable to '{expectedBaseType.AssemblyQualifiedName}'.");
109+
}
110+
111+
// If no IServiceProvider was specified, prefer .ctor() [if it exists]
112+
if (_services == null)
113+
{
114+
var ctorParameterless = implementationType.GetConstructor(Type.EmptyTypes);
115+
if (ctorParameterless != null)
116+
{
117+
return Activator.CreateInstance(implementationType);
118+
}
119+
}
120+
121+
// If an IServiceProvider was specified or if .ctor() doesn't exist, prefer .ctor(IServiceProvider) [if it exists]
122+
var ctorWhichTakesServiceProvider = implementationType.GetConstructor(new Type[] { typeof(IServiceProvider) });
123+
if (ctorWhichTakesServiceProvider != null)
124+
{
125+
return ctorWhichTakesServiceProvider.Invoke(new[] { _services });
126+
}
127+
128+
// Finally, prefer .ctor() as an ultimate fallback.
129+
// This will throw if the ctor cannot be called.
130+
return Activator.CreateInstance(implementationType);
131+
}
132+
}
133+
}

sdk/extensions/Azure.Extensions.AspNetCore.DataProtection.Keys/tests/Azure.Extensions.AspNetCore.DataProtection.Keys.Tests.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
<PackageReference Include="Microsoft.NET.Test.Sdk" />
1313
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
1414
<PackageReference Include="Moq" />
15+
16+
<PackageReference Include="Microsoft.AspNetCore.DataProtection.AzureKeyVault" VersionOverride="3.1.7" />
1517
</ItemGroup>
1618

1719
<ItemGroup>

sdk/extensions/Azure.Extensions.AspNetCore.DataProtection.Keys/tests/DataProtectionKeysFunctionalTests.cs

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
using Microsoft.Extensions.DependencyInjection;
1515
using NUnit.Framework;
1616

17-
namespace Azure.Extensions.AspNetCore.DataProtection.Blobs.Tests
17+
namespace Azure.Extensions.AspNetCore.DataProtection.Keys.Tests
1818
{
1919
public class DataProtectionKeysFunctionalTests
2020
{
@@ -55,7 +55,59 @@ public async Task ProtectsKeysWithKeyVaultKey()
5555

5656
foreach (var element in testKeyRepository.GetAllElements())
5757
{
58-
StringAssert.Contains("This key is encrypted with Azure KeyVault", element.ToString());
58+
StringAssert.Contains("This key is encrypted with Azure Key Vault", element.ToString());
59+
}
60+
}
61+
62+
[Test]
63+
[Category("Live")]
64+
public async Task CanUprotectExistingKeys()
65+
{
66+
var credential = new ClientSecretCredential(
67+
DataProtectionTestEnvironment.Instance.TenantId,
68+
DataProtectionTestEnvironment.Instance.ClientId,
69+
DataProtectionTestEnvironment.Instance.ClientSecret);
70+
var client = new KeyClient(new Uri(DataProtectionTestEnvironment.Instance.KeyVaultUrl), credential);
71+
var key = await client.CreateKeyAsync("TestEncryptionKey2", KeyType.Rsa);
72+
73+
var serviceCollection = new ServiceCollection();
74+
75+
var testKeyRepository = new TestKeyRepository();
76+
77+
AzureDataProtectionBuilderExtensions.ProtectKeysWithAzureKeyVault(
78+
serviceCollection.AddDataProtection(),
79+
key.Value.Id.AbsoluteUri,
80+
DataProtectionTestEnvironment.Instance.ClientId,
81+
DataProtectionTestEnvironment.Instance.ClientSecret);
82+
83+
serviceCollection.Configure<KeyManagementOptions>(options =>
84+
{
85+
options.XmlRepository = testKeyRepository;
86+
});
87+
88+
var servicesOld = serviceCollection.BuildServiceProvider();
89+
90+
var serviceCollectionNew = new ServiceCollection();
91+
serviceCollectionNew.AddDataProtection().ProtectKeysWithAzureKeyVault(key.Value.Id, credential);
92+
serviceCollectionNew.Configure<KeyManagementOptions>(options =>
93+
{
94+
options.XmlRepository = testKeyRepository;
95+
});
96+
97+
var dataProtector = servicesOld.GetService<IDataProtectionProvider>().CreateProtector("Fancy purpose");
98+
var protectedText = dataProtector.Protect("Hello world!");
99+
100+
var newServices = serviceCollectionNew.BuildServiceProvider();
101+
var newDataProtectionProvider = newServices.GetService<IDataProtectionProvider>().CreateProtector("Fancy purpose");
102+
var unprotectedText = newDataProtectionProvider.Unprotect(protectedText);
103+
104+
Assert.AreEqual("Hello world!", unprotectedText);
105+
106+
// double check that keys were protected with KeyVault
107+
108+
foreach (var element in testKeyRepository.GetAllElements())
109+
{
110+
StringAssert.Contains("This key is encrypted with Azure", element.ToString());
59111
}
60112
}
61113

0 commit comments

Comments
 (0)