From ee009982dc96171cf62ab4542aaf7c9cff984390 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Mon, 11 Sep 2017 08:51:56 -0700 Subject: [PATCH] Add KeyVault encryption to DataProtection (#273) --- DataProtection.sln | 39 +++++- samples/AzureKeyVault/AzureKeyVault.csproj | 20 +++ samples/AzureKeyVault/Program.cs | 44 +++++++ samples/AzureKeyVault/settings.json | 5 + .../AzureDataProtectionBuilderExtensions.cs | 118 ++++++++++++++++++ .../AzureKeyVaultXmlDecryptor.cs | 52 ++++++++ .../AzureKeyVaultXmlEncryptor.cs | 77 ++++++++++++ .../IKeyVaultWrappingClient.cs | 14 +++ .../KeyVaultClientWrapper.cs | 29 +++++ ...etCore.DataProtection.AzureKeyVault.csproj | 20 +++ .../Properties/AssemblyInfo.cs | 9 ++ .../XmlEncryption/XmlEncryptionExtensions.cs | 1 - .../AzureKeyVaultXmlEncryptorTests.cs | 78 ++++++++++++ ...e.DataProtection.AzureKeyVault.Test.csproj | 18 +++ 14 files changed, 521 insertions(+), 3 deletions(-) create mode 100644 samples/AzureKeyVault/AzureKeyVault.csproj create mode 100644 samples/AzureKeyVault/Program.cs create mode 100644 samples/AzureKeyVault/settings.json create mode 100644 src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs create mode 100644 src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj create mode 100644 src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs create mode 100644 test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs create mode 100644 test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj diff --git a/DataProtection.sln b/DataProtection.sln index ead0e13a92..4c1adcfabb 100644 --- a/DataProtection.sln +++ b/DataProtection.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26504.1 +VisualStudioVersion = 15.0.26814.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5FCB2DA3-5395-47F5-BCEE-E0EA319448EA}" EndProject @@ -10,7 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{5A3A EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E1D86B1B-41D8-43C9-97FD-C2BF65C414E2}" ProjectSection(SolutionItems) = preProject - build\common.props = build\common.props build\dependencies.props = build\dependencies.props NuGet.config = NuGet.config EndProjectSection @@ -55,6 +54,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "KeyManagementSample", "samp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomEncryptorSample", "samples\CustomEncryptorSample\CustomEncryptorSample.csproj", "{F4D59BBD-6145-4EE0-BA6E-AD03605BF151}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.AzureKeyVault", "src\Microsoft.AspNetCore.DataProtection.AzureKeyVault\Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj", "{4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureKeyVault", "samples\AzureKeyVault\AzureKeyVault.csproj", "{295E8539-5450-4764-B3F5-51F968628022}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test", "test\Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test\Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj", "{C85ED942-8121-453F-8308-9DB730843B63}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -219,6 +224,30 @@ Global {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Release|Any CPU.Build.0 = Release|Any CPU {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Release|x86.ActiveCfg = Release|Any CPU {F4D59BBD-6145-4EE0-BA6E-AD03605BF151}.Release|x86.Build.0 = Release|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|x86.ActiveCfg = Debug|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Debug|x86.Build.0 = Debug|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|Any CPU.Build.0 = Release|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|x86.ActiveCfg = Release|Any CPU + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9}.Release|x86.Build.0 = Release|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Debug|Any CPU.Build.0 = Debug|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Debug|x86.ActiveCfg = Debug|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Debug|x86.Build.0 = Debug|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Release|Any CPU.ActiveCfg = Release|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Release|Any CPU.Build.0 = Release|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Release|x86.ActiveCfg = Release|Any CPU + {295E8539-5450-4764-B3F5-51F968628022}.Release|x86.Build.0 = Release|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Debug|x86.ActiveCfg = Debug|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Debug|x86.Build.0 = Debug|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Release|Any CPU.Build.0 = Release|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Release|x86.ActiveCfg = Release|Any CPU + {C85ED942-8121-453F-8308-9DB730843B63}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -244,5 +273,11 @@ Global {32CF970B-E2F1-4CD9-8DB3-F5715475373A} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2} {6E066F8D-2910-404F-8949-F58125E28495} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2} {F4D59BBD-6145-4EE0-BA6E-AD03605BF151} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2} + {4E76B2A8-9DC3-46E6-B5FC-097A1D1DFBE9} = {5FCB2DA3-5395-47F5-BCEE-E0EA319448EA} + {295E8539-5450-4764-B3F5-51F968628022} = {5A3A5DE3-49AD-431C-971D-B01B62D94AE2} + {C85ED942-8121-453F-8308-9DB730843B63} = {60336AB3-948D-4D15-A5FB-F32A2B91E814} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DD305D75-BD1B-43AE-BF04-869DA6A0858F} EndGlobalSection EndGlobal diff --git a/samples/AzureKeyVault/AzureKeyVault.csproj b/samples/AzureKeyVault/AzureKeyVault.csproj new file mode 100644 index 0000000000..4907ff7925 --- /dev/null +++ b/samples/AzureKeyVault/AzureKeyVault.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp2.0 + exe + + + + + + + + + + + + + + + diff --git a/samples/AzureKeyVault/Program.cs b/samples/AzureKeyVault/Program.cs new file mode 100644 index 0000000000..7d6299f3e5 --- /dev/null +++ b/samples/AzureKeyVault/Program.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. 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.IO; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace ConsoleApplication +{ + public class Program + { + public static void Main(string[] args) + { + var builder = new ConfigurationBuilder(); + builder.SetBasePath(Directory.GetCurrentDirectory()); + builder.AddJsonFile("settings.json"); + var config = builder.Build(); + + var store = new X509Store(StoreLocation.CurrentUser); + store.Open(OpenFlags.ReadOnly); + var cert = store.Certificates.Find(X509FindType.FindByThumbprint, config["CertificateThumbprint"], false); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddLogging(); + serviceCollection.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(".")) + .ProtectKeysWithAzureKeyVault(config["KeyId"], config["ClientId"], cert.OfType().Single()); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + var loggerFactory = serviceProvider.GetService(); + loggerFactory.AddConsole(); + + var protector = serviceProvider.GetDataProtector("Test"); + + Console.WriteLine(protector.Protect("Hello world")); + } + } +} diff --git a/samples/AzureKeyVault/settings.json b/samples/AzureKeyVault/settings.json new file mode 100644 index 0000000000..ef7d4d81b8 --- /dev/null +++ b/samples/AzureKeyVault/settings.json @@ -0,0 +1,5 @@ +{ + "CertificateThumbprint": "", + "KeyId": "", + "ClientId": "" +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs new file mode 100644 index 0000000000..0701220b4b --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation. 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.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore.DataProtection.AzureKeyVault; +using Microsoft.AspNetCore.DataProtection.KeyManagement; +using Microsoft.Azure.KeyVault; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Clients.ActiveDirectory; + +namespace Microsoft.AspNetCore.DataProtection +{ + /// + /// Contains Azure KeyVault-specific extension methods for modifying a . + /// + public static class AzureDataProtectionBuilderExtensions + { + /// + /// Configures the data protection system to protect keys with specified key in Azure KeyVault. + /// + /// The builder instance to modify. + /// The Azure KeyVault key identifier used for key encryption. + /// The application client id. + /// + /// The value . + public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier, string clientId, X509Certificate2 certificate) + { + if (string.IsNullOrEmpty(clientId)) + { + throw new ArgumentException(nameof(clientId)); + } + if (certificate == null) + { + throw new ArgumentNullException(nameof(certificate)); + } + + KeyVaultClient.AuthenticationCallback callback = + (authority, resource, scope) => GetTokenFromClientCertificate(authority, resource, clientId, certificate); + + return ProtectKeysWithAzureKeyVault(builder, new KeyVaultClient(callback), keyIdentifier); + } + + private static async Task GetTokenFromClientCertificate(string authority, string resource, string clientId, X509Certificate2 certificate) + { + var authContext = new AuthenticationContext(authority); + var result = await authContext.AcquireTokenAsync(resource, new ClientAssertionCertificate(clientId, certificate)); + return result.AccessToken; + } + + /// + /// Configures the data protection system to protect keys with specified key in Azure KeyVault. + /// + /// The builder instance to modify. + /// The Azure KeyVault key identifier used for key encryption. + /// The application client id. + /// The client secret to use for authentication. + /// The value . + public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier, string clientId, string clientSecret) + { + if (string.IsNullOrEmpty(clientId)) + { + throw new ArgumentNullException(nameof(clientId)); + } + if (string.IsNullOrEmpty(clientSecret)) + { + throw new ArgumentNullException(nameof(clientSecret)); + } + + KeyVaultClient.AuthenticationCallback callback = + (authority, resource, scope) => GetTokenFromClientSecret(authority, resource, clientId, clientSecret); + + return ProtectKeysWithAzureKeyVault(builder, new KeyVaultClient(callback), keyIdentifier); + } + + private static async Task GetTokenFromClientSecret(string authority, string resource, string clientId, string clientSecret) + { + var authContext = new AuthenticationContext(authority); + var clientCred = new ClientCredential(clientId, clientSecret); + var result = await authContext.AcquireTokenAsync(resource, clientCred); + return result.AccessToken; + } + + /// + /// Configures the data protection system to protect keys with specified key in Azure KeyVault. + /// + /// The builder instance to modify. + /// The to use for KeyVault access. + /// The Azure KeyVault key identifier used for key encryption. + /// The value . + public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, KeyVaultClient client, string keyIdentifier) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + if (client == null) + { + throw new ArgumentNullException(nameof(client)); + } + if (string.IsNullOrEmpty(keyIdentifier)) + { + throw new ArgumentException(nameof(keyIdentifier)); + } + + var vaultClientWrapper = new KeyVaultClientWrapper(client); + + builder.Services.AddSingleton(vaultClientWrapper); + builder.Services.Configure(options => + { + options.XmlEncryptor = new AzureKeyVaultXmlEncryptor(vaultClientWrapper, keyIdentifier); + }); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs new file mode 100644 index 0000000000..b9942fa84f --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation. 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.IO; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.DataProtection.XmlEncryption; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault +{ + internal class AzureKeyVaultXmlDecryptor: IXmlDecryptor + { + private readonly IKeyVaultWrappingClient _client; + + public AzureKeyVaultXmlDecryptor(IServiceProvider serviceProvider) + { + _client = serviceProvider.GetService(); + } + + public XElement Decrypt(XElement encryptedElement) + { + return DecryptAsync(encryptedElement).GetAwaiter().GetResult(); + } + + private async Task DecryptAsync(XElement encryptedElement) + { + var kid = (string)encryptedElement.Element("kid"); + var symmetricKey = Convert.FromBase64String((string)encryptedElement.Element("key")); + var symmetricIV = Convert.FromBase64String((string)encryptedElement.Element("iv")); + + var encryptedValue = Convert.FromBase64String((string)encryptedElement.Element("value")); + + var result = await _client.UnwrapKeyAsync(kid, AzureKeyVaultXmlEncryptor.DefaultKeyEncryption, symmetricKey); + + byte[] decryptedValue; + using (var symmetricAlgorithm = AzureKeyVaultXmlEncryptor.DefaultSymmetricAlgorithmFactory()) + { + using (var decryptor = symmetricAlgorithm.CreateDecryptor(result.Result, symmetricIV)) + { + decryptedValue = decryptor.TransformFinalBlock(encryptedValue, 0, encryptedValue.Length); + } + } + + using (var memoryStream = new MemoryStream(decryptedValue)) + { + return XElement.Load(memoryStream); + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs new file mode 100644 index 0000000000..3451c3ded2 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. 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.IO; +using System.Security.Cryptography; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.DataProtection.XmlEncryption; +using Microsoft.Azure.KeyVault.WebKey; + +namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault +{ + internal class AzureKeyVaultXmlEncryptor : IXmlEncryptor + { + internal static string DefaultKeyEncryption = JsonWebKeyEncryptionAlgorithm.RSAOAEP; + internal static Func DefaultSymmetricAlgorithmFactory = Aes.Create; + + private readonly RandomNumberGenerator _randomNumberGenerator; + private readonly IKeyVaultWrappingClient _client; + private readonly string _keyId; + + public AzureKeyVaultXmlEncryptor(IKeyVaultWrappingClient client, string keyId) + : this(client, keyId, RandomNumberGenerator.Create()) + { + } + + internal AzureKeyVaultXmlEncryptor(IKeyVaultWrappingClient client, string keyId, RandomNumberGenerator randomNumberGenerator) + { + _client = client; + _keyId = keyId; + _randomNumberGenerator = randomNumberGenerator; + } + + public EncryptedXmlInfo Encrypt(XElement plaintextElement) + { + return EncryptAsync(plaintextElement).GetAwaiter().GetResult(); + } + + private async Task EncryptAsync(XElement plaintextElement) + { + byte[] value; + using (var memoryStream = new MemoryStream()) + { + plaintextElement.Save(memoryStream, SaveOptions.DisableFormatting); + value = memoryStream.ToArray(); + } + + using (var symmetricAlgorithm = DefaultSymmetricAlgorithmFactory()) + { + var symmetricBlockSize = symmetricAlgorithm.BlockSize / 8; + var symmetricKey = new byte[symmetricBlockSize]; + var symmetricIV = new byte[symmetricBlockSize]; + _randomNumberGenerator.GetBytes(symmetricKey); + _randomNumberGenerator.GetBytes(symmetricIV); + + byte[] encryptedValue; + using (var encryptor = symmetricAlgorithm.CreateEncryptor(symmetricKey, symmetricIV)) + { + encryptedValue = encryptor.TransformFinalBlock(value, 0, value.Length); + } + + var wrappedKey = await _client.WrapKeyAsync(_keyId, DefaultKeyEncryption, symmetricKey); + + var element = new XElement("encryptedKey", + new XComment(" This key is encrypted with Azure KeyVault. "), + new XElement("kid", wrappedKey.Kid), + new XElement("key", Convert.ToBase64String(wrappedKey.Result)), + new XElement("iv", Convert.ToBase64String(symmetricIV)), + new XElement("value", Convert.ToBase64String(encryptedValue))); + + return new EncryptedXmlInfo(element, typeof(AzureKeyVaultXmlDecryptor)); + } + + } + } +} diff --git a/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs new file mode 100644 index 0000000000..2347460dc3 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/IKeyVaultWrappingClient.cs @@ -0,0 +1,14 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.Azure.KeyVault.Models; + +namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault +{ + internal interface IKeyVaultWrappingClient + { + Task UnwrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText); + Task WrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText); + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs new file mode 100644 index 0000000000..82fe0649e2 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/KeyVaultClientWrapper.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Threading.Tasks; +using Microsoft.Azure.KeyVault; +using Microsoft.Azure.KeyVault.Models; + +namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault +{ + internal class KeyVaultClientWrapper : IKeyVaultWrappingClient + { + private readonly KeyVaultClient _client; + + public KeyVaultClientWrapper(KeyVaultClient client) + { + _client = client; + } + + public Task UnwrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText) + { + return _client.UnwrapKeyAsync(keyIdentifier, algorithm, cipherText); + } + + public Task WrapKeyAsync(string keyIdentifier, string algorithm, byte[] cipherText) + { + return _client.WrapKeyAsync(keyIdentifier, algorithm, cipherText); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj new file mode 100644 index 0000000000..ee7b42ab87 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj @@ -0,0 +1,20 @@ + + + + Microsoft Azure KeyVault key encryption support. + netstandard2.0 + true + aspnetcore;dataprotection;azure;keyvault + false + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..c23a3410b7 --- /dev/null +++ b/src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/Properties/AssemblyInfo.cs @@ -0,0 +1,9 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Reflection; +using System.Resources; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs index 74189cfad1..cfc65a44a2 100644 --- a/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs +++ b/src/Microsoft.AspNetCore.DataProtection/XmlEncryption/XmlEncryptionExtensions.cs @@ -46,7 +46,6 @@ namespace Microsoft.AspNetCore.DataProtection.XmlEncryption // the original document or other data structures. The element we pass to // the decryptor should be the child of the 'encryptedSecret' element. var clonedElementWhichRequiresDecryption = new XElement(elementWhichRequiresDecryption); - var innerDoc = new XDocument(clonedElementWhichRequiresDecryption); string decryptorTypeName = (string)clonedElementWhichRequiresDecryption.Attribute(XmlConstants.DecryptorTypeAttributeName); var decryptorInstance = activator.CreateInstance(decryptorTypeName); var decryptedElement = decryptorInstance.Decrypt(clonedElementWhichRequiresDecryption.Elements().Single()); diff --git a/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs b/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs new file mode 100644 index 0000000000..faa9bd1c96 --- /dev/null +++ b/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/AzureKeyVaultXmlEncryptorTests.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Linq; +using System.Security.Cryptography; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.Azure.KeyVault.Models; +using Microsoft.Azure.KeyVault.WebKey; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test +{ + public class AzureKeyVaultXmlEncryptorTests + { + [Fact] + public void UsesKeyVaultToEncryptKey() + { + var mock = new Mock(); + mock.Setup(client => client.WrapKeyAsync("key", JsonWebKeyEncryptionAlgorithm.RSAOAEP, It.IsAny())) + .Returns((_, __, data) => Task.FromResult(new KeyOperationResult("KeyId", data.Reverse().ToArray()))); + + var encryptor = new AzureKeyVaultXmlEncryptor(mock.Object, "key", new MockNumberGenerator()); + var result = encryptor.Encrypt(new XElement("Element")); + + var encryptedElement = result.EncryptedElement; + var value = encryptedElement.Element("value"); + + mock.VerifyAll(); + Assert.NotNull(result); + Assert.NotNull(value); + Assert.Equal(typeof(AzureKeyVaultXmlDecryptor), result.DecryptorType); + Assert.Equal("VfLYL2prdymawfucH3Goso0zkPbQ4/GKqUsj2TRtLzsBPz7p7cL1SQaY6I29xSlsPQf6IjxHSz4sDJ427GvlLQ==", encryptedElement.Element("value").Value); + Assert.Equal("AAECAwQFBgcICQoLDA0ODw==", encryptedElement.Element("iv").Value); + Assert.Equal("Dw4NDAsKCQgHBgUEAwIBAA==", encryptedElement.Element("key").Value); + Assert.Equal("KeyId", encryptedElement.Element("kid").Value); + } + + [Fact] + public void UsesKeyVaultToDecryptKey() + { + var mock = new Mock(); + mock.Setup(client => client.UnwrapKeyAsync("KeyId", JsonWebKeyEncryptionAlgorithm.RSAOAEP, It.IsAny())) + .Returns((_, __, data) => Task.FromResult(new KeyOperationResult(null, data.Reverse().ToArray()))) + .Verifiable(); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(mock.Object); + + var encryptor = new AzureKeyVaultXmlDecryptor(serviceCollection.BuildServiceProvider()); + + var result = encryptor.Decrypt(XElement.Parse( + @" + KeyId + Dw4NDAsKCQgHBgUEAwIBAA== + AAECAwQFBgcICQoLDA0ODw== + VfLYL2prdymawfucH3Goso0zkPbQ4/GKqUsj2TRtLzsBPz7p7cL1SQaY6I29xSlsPQf6IjxHSz4sDJ427GvlLQ== + ")); + + mock.VerifyAll(); + Assert.NotNull(result); + Assert.Equal("", result.ToString()); + } + + private class MockNumberGenerator : RandomNumberGenerator + { + public override void GetBytes(byte[] data) + { + for (int i = 0; i < data.Length; i++) + { + data[i] = (byte)i; + } + } + } + } +} diff --git a/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj b/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj new file mode 100644 index 0000000000..6983aebb33 --- /dev/null +++ b/test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test/Microsoft.AspNetCore.DataProtection.AzureKeyVault.Test.csproj @@ -0,0 +1,18 @@ + + + + netcoreapp2.0;net461 + netcoreapp2.0 + true + + + + + + + + + + + +