forked from Azure/azure-sdk-for-net
-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add KeyVault.Secrets DataProtection extensions (Azure#9953)
- Loading branch information
Showing
13 changed files
with
368 additions
and
3 deletions.
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
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
9 changes: 9 additions & 0 deletions
9
...rotection/api/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.netstandard2.0.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,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; } | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
...tCore.DataProtection/src/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.csproj
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> | ||
<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> |
67 changes: 67 additions & 0 deletions
67
....Secrets.AspNetCore.DataProtection/src/AzureDataProtectionKeyVaultKeyBuilderExtensions.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,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; | ||
} | ||
} | ||
} |
56 changes: 56 additions & 0 deletions
56
...zure.Security.KeyVault.Secrets.AspNetCore.DataProtection/src/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,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); | ||
} | ||
} | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
...zure.Security.KeyVault.Secrets.AspNetCore.DataProtection/src/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,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)); | ||
} | ||
|
||
} | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
.../Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection/src/Properties/AssemblyInfo.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,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")] |
22 changes: 22 additions & 0 deletions
22
...ataProtection/test/Azure.Security.KeyVault.Secrets.AspNetCore.DataProtection.Tests.csproj
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,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> |
92 changes: 92 additions & 0 deletions
92
...ecurity.KeyVault.Secrets.AspNetCore.DataProtection/test/AzureKeyVaultXmlEncryptorTests.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,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; | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.