Skip to content

Commit

Permalink
Add KeyVault.Secrets DataProtection extensions (Azure#9953)
Browse files Browse the repository at this point in the history
  • Loading branch information
pakrym authored Feb 14, 2020
1 parent 79f10ab commit f0f7dd3
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 3 deletions.
1 change: 1 addition & 0 deletions eng/.docsettings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ known_presence_issues:
- ['sdk/keyvault','#5499']
- ['sdk/eventhub','#5499']
- ['sdk/attestation/Microsoft.Azure.Attestation','#5499']
- ['sdk/keyvault/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection','#9955']

# List for changelogs begins here
- ['sdk/applicationinsights/Microsoft.Azure.ApplicationInsights.Query/CHANGELOG.md','#5499']
Expand Down
1 change: 1 addition & 0 deletions eng/Directory.Build.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@

<!-- The set of tags that should be added to the packages -->
<PackageCommonTags>windowsazureofficial;azureofficial</PackageCommonTags>
<AzureCoreSharedSources>$(MSBuildThisFileDirectory)/../sdk/core/Azure.Core/src/Shared/</AzureCoreSharedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(IsTestProject)' == 'true' or '$(IsTestSupportProject)' == 'true' or '$(IsSamplesProject)' == 'true'">
Expand Down
1 change: 1 addition & 0 deletions eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
<PackageReference Update="Azure.ClientSdk.Analyzers" Version="0.1.1-dev.20200116.2" />
<PackageReference Update="System.Memory" Version="4.5.3" />
<PackageReference Update="Microsoft.Bcl.AsyncInterfaces" Version="1.0.0" />
<PackageReference Update="Microsoft.AspNetCore.DataProtection" Version="2.1.0" />
<PackageReference Update="Microsoft.Extensions.DependencyInjection.Abstractions" Version="2.1.0" />
<PackageReference Update="Microsoft.Extensions.DependencyInjection" Version="2.1.0" />
<PackageReference Update="Microsoft.Extensions.Logging.Abstractions" Version="2.1.0" />
Expand Down
3 changes: 0 additions & 3 deletions sdk/core/Azure.Core/src/Azure.Core.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AzureCoreSharedSources>$(MSBuildThisFileDirectory)\Shared\</AzureCoreSharedSources>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Core" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Microsoft.AspNetCore.DataProtection
{
public static partial class AzureDataProtectionKeyVaultKeyBuilderExtensions
{
public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder builder, Azure.Core.Cryptography.IKeyEncryptionKeyResolver client, string keyIdentifier) { throw null; }
public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder builder, string keyIdentifier) { throw null; }
public static Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this Microsoft.AspNetCore.DataProtection.IDataProtectionBuilder builder, string keyIdentifier, Azure.Core.TokenCredential tokenCredential) { throw null; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Microsoft Azure Key Vault key encryption support.</Description>
<PackageTags>aspnetcore;dataprotection;azure;keyvault</PackageTags>
<Version>1.0.0-preview.1</Version>
<EnableApiCompat>false</EnableApiCompat>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.DataProtection" />
<PackageReference Include="Azure.Security.KeyVault.Keys" />
<PackageReference Include="Azure.Identity" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(AzureCoreSharedSources)Argument.cs" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using Azure.Core;
using Azure.Core.Cryptography;
using Azure.Identity;
using Azure.Security.KeyVault.Keys.Cryptography;
using Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;

#pragma warning disable AZC0001 // Extension methods have to be in the correct namespace to appear in intellisense.
namespace Microsoft.AspNetCore.DataProtection
#pragma warning disable
{
/// <summary>
/// Contains Azure KeyVault-specific extension methods for modifying a <see cref="IDataProtectionBuilder"/>.
/// </summary>
public static class AzureDataProtectionKeyVaultKeyBuilderExtensions
{
/// <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 Key Vault key identifier used for key encryption.</param>
/// <returns>The value <paramref name="builder"/>.</returns>
public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier)
{
return ProtectKeysWithAzureKeyVault(builder, keyIdentifier, new DefaultAzureCredential());
}

/// <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 Key Vault key identifier used for key encryption.</param>
/// <param name="tokenCredential">The token credential to use for authentication.</param>
/// <returns>The value <paramref name="builder"/>.</returns>
public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, string keyIdentifier, TokenCredential tokenCredential)
{
return ProtectKeysWithAzureKeyVault(builder, new KeyResolver(tokenCredential), keyIdentifier);
}

/// <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="client">The <see cref="IKeyEncryptionKeyResolver"/> to use for Key Vault access.</param>
/// <param name="keyIdentifier">The Azure Key Vault key identifier used for key encryption.</param>
/// <returns>The value <paramref name="builder"/>.</returns>
public static IDataProtectionBuilder ProtectKeysWithAzureKeyVault(this IDataProtectionBuilder builder, IKeyEncryptionKeyResolver client, string keyIdentifier)
{
Argument.AssertNotNull(builder, nameof(builder));
Argument.AssertNotNull(client, nameof(client));
Argument.AssertNotNullOrEmpty(keyIdentifier, nameof(keyIdentifier));

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

return builder;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Linq;
using Azure.Core.Cryptography;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
using Microsoft.Extensions.DependencyInjection;

namespace Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection
{
#pragma warning disable CA1812 // False positive, AzureKeyVaultXmlDecryptor is used in AzureKeyVaultXmlEncryptor
internal class AzureKeyVaultXmlDecryptor : IXmlDecryptor
#pragma warning restore
{
private readonly IKeyEncryptionKeyResolver _client;

public AzureKeyVaultXmlDecryptor(IServiceProvider serviceProvider)
{
_client = serviceProvider.GetService<IKeyEncryptionKeyResolver>();
}

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 key = await _client.ResolveAsync(kid).ConfigureAwait(false);
var result = await key.UnwrapKeyAsync(AzureKeyVaultXmlEncryptor.DefaultKeyEncryption, symmetricKey).ConfigureAwait(false);

byte[] decryptedValue;
using (var symmetricAlgorithm = AzureKeyVaultXmlEncryptor.DefaultSymmetricAlgorithmFactory())
{
using (var decryptor = symmetricAlgorithm.CreateDecryptor(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,80 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Xml.Linq;
using Azure.Core.Cryptography;
using Azure.Security.KeyVault.Keys.Cryptography;
using Microsoft.AspNetCore.DataProtection.XmlEncryption;

namespace Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection
{
internal class AzureKeyVaultXmlEncryptor : IXmlEncryptor
{
internal static readonly string DefaultKeyEncryption = KeyWrapAlgorithm.RsaOaep.ToString();
internal static readonly Func<SymmetricAlgorithm> DefaultSymmetricAlgorithmFactory = Aes.Create;

private readonly RandomNumberGenerator _randomNumberGenerator;
private readonly IKeyEncryptionKeyResolver _client;
private readonly string _keyId;

public AzureKeyVaultXmlEncryptor(IKeyEncryptionKeyResolver client, string keyId)
: this(client, keyId, RandomNumberGenerator.Create())
{
}

internal AzureKeyVaultXmlEncryptor(IKeyEncryptionKeyResolver 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;
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 key = await _client.ResolveAsync(_keyId).ConfigureAwait(false);
var wrappedKey = await key.WrapKeyAsync(DefaultKeyEncryption, symmetricKey).ConfigureAwait(false);

var element = new XElement("encryptedKey",
new XComment(" This key is encrypted with Azure KeyVault. "),
new XElement("kid", key.KeyId),
new XElement("key", Convert.ToBase64String(wrappedKey)),
new XElement("iv", Convert.ToBase64String(symmetricIV)),
new XElement("value", Convert.ToBase64String(encryptedValue)));

return new EncryptedXmlInfo(element, typeof(AzureKeyVaultXmlDecryptor));
}

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]
[assembly: InternalsVisibleTo("Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")]
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>
<ExcludeRecordingFramework>true</ExcludeRecordingFramework>
</PropertyGroup>

<Import Project="..\..\..\core\Azure.Core\tests\TestFramework.props" />

<ItemGroup>
<PackageReference Include="nunit" />
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
<PackageReference Include="Moq" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\src\Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Xml.Linq;
using Azure.Core.Cryptography;
using Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using NUnit.Framework;

namespace Microsoft.AspNetCore.DataProtection.Azure.KeyVault
{
public class AzureKeyVaultXmlEncryptorTests
{
[Test]
public void UsesKeyVaultToEncryptKey()
{
var keyMock = new Mock<IKeyEncryptionKey>(MockBehavior.Strict);
keyMock.Setup(client => client.WrapKeyAsync("RSA-OAEP", It.IsAny<ReadOnlyMemory<byte>>(), default))
.ReturnsAsync((string _, ReadOnlyMemory<byte> data, CancellationToken __) => data.ToArray().Reverse().ToArray())
.Verifiable();

keyMock.SetupGet(client => client.KeyId).Returns("KeyId");

var mock = new Mock<IKeyEncryptionKeyResolver>();
mock.Setup(client => client.ResolveAsync("key", default))
.ReturnsAsync((string _, CancellationToken __) => keyMock.Object)
.Verifiable();

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.AreEqual(typeof(AzureKeyVaultXmlDecryptor), result.DecryptorType);
Assert.AreEqual("VfLYL2prdymawfucH3Goso0zkPbQ4/GKqUsj2TRtLzsBPz7p7cL1SQaY6I29xSlsPQf6IjxHSz4sDJ427GvlLQ==", encryptedElement.Element("value").Value);
Assert.AreEqual("AAECAwQFBgcICQoLDA0ODw==", encryptedElement.Element("iv").Value);
Assert.AreEqual("Dw4NDAsKCQgHBgUEAwIBAA==", encryptedElement.Element("key").Value);
Assert.AreEqual("KeyId", encryptedElement.Element("kid").Value);
}

[Test]
public void UsesKeyVaultToDecryptKey()
{
var keyMock = new Mock<IKeyEncryptionKey>(MockBehavior.Strict);
keyMock.Setup(client => client.UnwrapKeyAsync("RSA-OAEP", It.IsAny<ReadOnlyMemory<byte>>(), default))
.ReturnsAsync((string _, ReadOnlyMemory<byte> data, CancellationToken __) => data.ToArray().Reverse().ToArray())
.Verifiable();

var mock = new Mock<IKeyEncryptionKeyResolver>();
mock.Setup(client => client.ResolveAsync("KeyId", default))
.ReturnsAsync((string _, CancellationToken __) => keyMock.Object)
.Verifiable();

var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(mock.Object);

var encryptor = new AzureKeyVaultXmlDecryptor(serviceCollection.BuildServiceProvider());

var result = encryptor.Decrypt(XElement.Parse(
@"<encryptedKey>
<kid>KeyId</kid>
<key>Dw4NDAsKCQgHBgUEAwIBAA==</key>
<iv>AAECAwQFBgcICQoLDA0ODw==</iv>
<value>VfLYL2prdymawfucH3Goso0zkPbQ4/GKqUsj2TRtLzsBPz7p7cL1SQaY6I29xSlsPQf6IjxHSz4sDJ427GvlLQ==</value>
</encryptedKey>"));

mock.VerifyAll();
Assert.NotNull(result);
Assert.AreEqual("<Element />", result.ToString());
}

private class MockNumberGenerator : RandomNumberGenerator
{
public override void GetBytes(byte[] data)
{
for (int i = 0; i < data.Length; i++)
{
data[i] = (byte)i;
}
}
}
}
}
Loading

0 comments on commit f0f7dd3

Please sign in to comment.