This repository has been archived by the owner on Oct 17, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 87
Add KeyVault encryption to DataProtection #273
Merged
Merged
Changes from 6 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
65cd131
Add KeyVault encryption to DataProtection
pakrym 27e995b
csproj all the things
pakrym 7b6526b
Whitespace
pakrym 09afee1
Fix issues
pakrym 3b0ce4e
Remove versions
pakrym d166df6
DataProtection.sln.DotSettings
pakrym 52e1b4f
(C)
pakrym 15e2261
WS
pakrym e67d86c
PR comments
pakrym 0220392
More string checks
pakrym File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)" /> | ||
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"CertificateThumbprint": "", | ||
"KeyId": "", | ||
"ClientId": "" | ||
} |
118 changes: 118 additions & 0 deletions
118
...Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureDataProtectionBuilderExtensions.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
{ | ||
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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlDecryptor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} | ||
} |
77 changes: 77 additions & 0 deletions
77
src/Microsoft.AspNetCore.DataProtection.AzureKeyVault/AzureKeyVaultXmlEncryptor.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @blowdart can please check that this is done correctly? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)); | ||
} | ||
|
||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: remove
Version="$(AspNetCoreVersion)"