Skip to content
This repository has been archived by the owner on Oct 17, 2018. It is now read-only.

Add KeyVault encryption to DataProtection #273

Merged
merged 10 commits into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions DataProtection.sln
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
20 changes: 20 additions & 0 deletions samples/AzureKeyVault/AzureKeyVault.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<OutputType>exe</OutputType>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.DataProtection.AzureKeyVault\Microsoft.AspNetCore.DataProtection.AzureKeyVault.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="$(AspNetCoreVersion)" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: remove Version="$(AspNetCoreVersion)"

<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="$(AspNetCoreVersion)" />
</ItemGroup>

</Project>
42 changes: 42 additions & 0 deletions samples/AzureKeyVault/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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<X509Certificate2>().Single());

var serviceProvider = serviceCollection.BuildServiceProvider();

var loggerFactory = serviceProvider.GetService<ILoggerFactory>();
loggerFactory.AddConsole();

var protector = serviceProvider.GetDataProtector("Test");

Console.WriteLine(protector.Protect("Hello world"));
}
}
}
5 changes: 5 additions & 0 deletions samples/AzureKeyVault/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"CertificateThumbprint": "",
"KeyId": "",
"ClientId": ""
}
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Contains Azure KeyVault-specific extension methods for modifying a <see cref="IDataProtectionBuilder"/>.
/// </summary>
public static class AzureDataProtectionBuilderExtensions
{
/// <summary>
/// Configures the data protection system to protect keys with specified key in Azure KeyVault.
/// </summary>
/// <param name="builder">The builder instance to modify.</param>
/// <param name="keyIdentifier">The Azure KeyVault key identifier used for key encryption.</param>
/// <param name="clientId">The application client id.</param>
/// <param name="certificate"></param>
/// <returns>The value <paramref name="builder"/>.</returns>
public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier, string clientId, X509Certificate2 certificate)
{
if (clientId == null)
{
throw new ArgumentNullException(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<string> 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;
}

/// <summary>
/// Configures the data protection system to protect keys with specified key in Azure KeyVault.
/// </summary>
/// <param name="builder">The builder instance to modify.</param>
/// <param name="keyIdentifier">The Azure KeyVault key identifier used for key encryption.</param>
/// <param name="clientId">The application client id.</param>
/// <param name="clientSecret">The client secret to use for authentication.</param>
/// <returns>The value <paramref name="builder"/>.</returns>
public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier, string clientId, string clientSecret)
{
if (clientId == null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

string.IsNullOrEmpty for string parameters

{
throw new ArgumentNullException(nameof(clientId));
}
if (clientSecret == null)
{
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<string> 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;
}

/// <summary>
/// Configures the data protection system to protect keys with specified key in Azure KeyVault.
/// </summary>
/// <param name="builder">The builder instance to modify.</param>
/// <param name="keyIdentifier">The Azure KeyVault key identifier used for key encryption.</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: params misordered.

/// <param name="client">The <see cref="KeyVaultClient"/> to use for KeyVault access.</param>
/// <returns>The value <paramref name="builder"/>.</returns>
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 (keyIdentifier == null)
{
throw new ArgumentNullException(nameof(keyIdentifier));
}

var vaultClientWrapper = new KeyVaultClientWrapper(client);

builder.Services.AddSingleton<IKeyVaultWrappingClient>(vaultClientWrapper);
builder.Services.Configure<KeyManagementOptions>(options =>
{
options.XmlEncryptor = new AzureKeyVaultXmlEncryptor(vaultClientWrapper, keyIdentifier);
});

return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<IKeyVaultWrappingClient>();
}

public XElement Decrypt(XElement encryptedElement)
{
return DecryptAsync(encryptedElement).GetAwaiter().GetResult();
}

private async Task<XElement> 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<SymmetricAlgorithm> 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<EncryptedXmlInfo> 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;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blowdart can please check that this is done correctly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That works :)

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));
}

}
}
}
Loading