Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port clientside encryption to mono-dll #12183

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
16 changes: 10 additions & 6 deletions sdk/storage/Azure.Storage.Blobs/src/BlobBaseClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -659,16 +658,20 @@ private async Task<Response<BlobDownloadInfo>> DownloadInternal(
bool async,
CancellationToken cancellationToken)
{
HttpRange requestedRange = range;
using (Pipeline.BeginLoggingScope(nameof(BlobBaseClient)))
{
Pipeline.LogMethodEnter(nameof(BlobBaseClient), message: $"{nameof(Uri)}: {Uri}");
try
{
EncryptedBlobRange encryptedRange = new EncryptedBlobRange(range);
if (UsingClientSideEncryption)
{
range = new EncryptedBlobRange(range).AdjustedRange;
}

// Start downloading the blob
(Response<FlattenedDownloadProperties> response, Stream stream) = await StartDownloadAsync(
UsingClientSideEncryption ? encryptedRange.AdjustedRange : range,
range,
conditions,
rangeGetContentHash,
async: async,
Expand All @@ -688,7 +691,7 @@ private async Task<Response<BlobDownloadInfo>> DownloadInternal(
stream,
startOffset =>
StartDownloadAsync(
UsingClientSideEncryption ? encryptedRange.AdjustedRange : range,
range,
conditions,
rangeGetContentHash,
startOffset,
Expand All @@ -698,7 +701,7 @@ private async Task<Response<BlobDownloadInfo>> DownloadInternal(
.Item2,
async startOffset =>
(await StartDownloadAsync(
UsingClientSideEncryption ? encryptedRange.AdjustedRange : range,
range,
conditions,
rangeGetContentHash,
startOffset,
Expand All @@ -713,7 +716,7 @@ private async Task<Response<BlobDownloadInfo>> DownloadInternal(
// we already return a nonseekable stream; returning a crypto stream is fine
if (UsingClientSideEncryption)
{
stream = await ClientSideDecryptInternal(stream, response.Value.Metadata, encryptedRange.OriginalRange, response.Value.ContentRange, async, cancellationToken).ConfigureAwait(false);
stream = await ClientSideDecryptInternal(stream, response.Value.Metadata, requestedRange, response.Value.ContentRange, async, cancellationToken).ConfigureAwait(false);
}

response.Value.Content = stream;
Expand Down Expand Up @@ -3003,6 +3006,7 @@ private async Task<Stream> ClientSideDecryptInternal(

bool ivInStream = originalRange.Offset >= 16;
kasobol-msft marked this conversation as resolved.
Show resolved Hide resolved

// this method throws when key cannot be resolved. Blobs is intended to throw on this failure.
var plaintext = await Utility.DecryptInternal(
content,
encryptionData,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.Cryptography;

namespace Azure.Storage.Blobs.Tests
{
internal class AlwaysFailsKeyEncryptionKeyResolver : IKeyEncryptionKeyResolver
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: it doesn't always fail.

Btw. I'd use Moq for such simple case rather than creating new type. I believe we have it as dependency.

Copy link
Member Author

Choose a reason for hiding this comment

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

addressed in upcoming push

{
/// <summary>
/// False means the resolver just can't find the key and returns null.
/// True means the resolver has an internal failure and throws.
/// </summary>
public bool ResolverInternalFailure { get; set; } = false;

public IKeyEncryptionKey Resolve(string keyId, CancellationToken cancellationToken = default)
{
if (ResolverInternalFailure)
{
throw new Exception();
}
return default;
}

public Task<IKeyEncryptionKey> ResolveAsync(string keyId, CancellationToken cancellationToken = default)
{
if (ResolverInternalFailure)
{
throw new Exception();
}
return Task.FromResult<IKeyEncryptionKey>(default);
}
}
}
50 changes: 50 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/tests/ClientSideEncryptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Azure.Core.Cryptography;
using Azure.Core.TestFramework;
using Azure.Security.KeyVault.Keys.Cryptography;
using Azure.Storage.Blobs.Tests;
using Azure.Storage.Cryptography;
using Azure.Storage.Cryptography.Models;
using Azure.Storage.Test.Shared;
Expand Down Expand Up @@ -339,6 +341,54 @@ public async Task RoundtripWithKeyvaultProvider()
}
}

[TestCase(true)]
[TestCase(false)]
[LiveOnly]
public async Task CannotFindKeyAsync(bool resolverFailure)
{
var data = GetRandomBuffer(Constants.KB);
var mockKey = new MockKeyEncryptionKey();
await using (var disposable = await GetTestContainerEncryptionAsync(
new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
{
KeyEncryptionKey = mockKey,
KeyResolver = mockKey,
KeyWrapAlgorithm = ThrowawayAlgorithmName
}))
{
var blob = disposable.Container.GetBlobClient(GetNewBlobName());
await blob.UploadAsync(new MemoryStream(data));

bool threwKeyNotFound = false;
bool threwGeneral = false;
try
{
// download but can't find key
var options = GetOptions();
options._clientSideEncryptionOptions = new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
{
KeyResolver = new AlwaysFailsKeyEncryptionKeyResolver() { ResolverInternalFailure = resolverFailure },
KeyWrapAlgorithm = "test"
};
var encryptedDataStream = new MemoryStream();
await new BlobClient(blob.Uri, GetNewSharedKeyCredentials(), options).DownloadToAsync(encryptedDataStream);
}
catch (ClientSideEncryptionKeyNotFoundException)
{
threwKeyNotFound = true;
}
catch (Exception)
{
threwGeneral = true;
}
finally
{
Assert.AreEqual(resolverFailure, threwGeneral);
Assert.AreEqual(!resolverFailure, threwKeyNotFound);
}
}
}

[Test]
[LiveOnly] // cannot seed content encryption key
[Ignore("stress test")]
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this just fetch and decrypt 10 times 10MB? If so maybe we could enable this.

Copy link
Member Author

Choose a reason for hiding this comment

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

This test alone runs quite a while last I ran it (that was a dinky laptop, though). People pushed me for an ignore attribute. I can check again if it's viable to run, especially since it's live only.

Copy link
Contributor

Choose a reason for hiding this comment

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

If you manage to run it reasonably fast then consider changing name of this test.

Copy link
Member Author

Choose a reason for hiding this comment

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

This test is about 16x longer than the rest of the clientside encryption tests combined, passing 8 minutes.

Copy link
Member

Choose a reason for hiding this comment

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

8 min isn't going to fly as a test we run locally, I vote we continue to leave this test marked as ignore.

Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
namespace Azure.Storage
{
public partial class ClientSideEncryptionKeyNotFoundException : System.Exception
{
public ClientSideEncryptionKeyNotFoundException(string keyId) { }
public string KeyId { get { throw null; } }
}
public partial class ClientSideEncryptionOptions
{
public ClientSideEncryptionOptions(Azure.Storage.ClientSideEncryptionVersion version) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Azure.Storage
{
/// <summary>
/// Thrown when the client fails to resolve the necessary key to decrypt data using client-side encryption.
/// </summary>
public class ClientSideEncryptionKeyNotFoundException : Exception
{
/// <summary>
/// Key that could not be resolved.
/// </summary>
public string KeyId { get; }

/// <summary>
/// Constructs the exception.
/// </summary>
/// <param name="keyId">Key id.</param>
public ClientSideEncryptionKeyNotFoundException(string keyId)
: base($"Provided key resolver ould not resolve key of id `{keyId}`.")
{
KeyId = keyId;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,5 @@ public ClientSideEncryptionOptions(ClientSideEncryptionVersion version)
{
Version = version;
}

/// <summary>
/// Copy constructor to keep these options grouped in clients while stopping users from
/// accidentally altering our configs out from under us.
/// </summary>
/// <param name="other"></param>
internal ClientSideEncryptionOptions(ClientSideEncryptionOptions other)
{
Version = other.Version;
KeyEncryptionKey = other.KeyEncryptionKey;
KeyResolver = other.KeyResolver;
KeyWrapAlgorithm = other.KeyWrapAlgorithm;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ namespace Azure.Storage.Cryptography
{
internal static class EncryptionErrors
{
public static ArgumentException KeyNotFound(string keyId)
=> new ArgumentException($"Could not resolve key of id `{keyId}`.");
public static ClientSideEncryptionKeyNotFoundException KeyNotFound(string keyId)
=> new ClientSideEncryptionKeyNotFoundException(keyId);

public static ArgumentException BadEncryptionAgent(string agent)
=> new ArgumentException("Invalid Encryption Agent. This version of the client library does not understand" +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ internal class EncryptionData
public string EncryptionMode { get; set; }
tg-msft marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// A <see cref="WrappedKey"/> object that stores the wrapping algorithm, key identifier and the encrypted key.
/// A <see cref="KeyEnvelope"/> object that stores the wrapping algorithm, key identifier and the encrypted key.
/// </summary>
public WrappedKey WrappedContentKey { get; set; }
public KeyEnvelope WrappedContentKey { get; set; }

/// <summary>
/// The encryption agent.
Expand Down Expand Up @@ -79,7 +79,7 @@ internal static async Task<EncryptionData> CreateInternalV1_0(
{
{ EncryptionConstants.AgentMetadataKey, AgentString }
},
WrappedContentKey = new WrappedKey()
WrappedContentKey = new KeyEnvelope()
{
Algorithm = keyWrapAlgorithm,
EncryptedKey = async
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public static void WriteEncryptionData(Utf8JsonWriter json, EncryptionData data)
json.WriteEndObject();
}

private static void WriteWrappedKey(Utf8JsonWriter json, WrappedKey key)
private static void WriteWrappedKey(Utf8JsonWriter json, KeyEnvelope key)
{
json.WriteString(nameof(key.KeyId), key.KeyId);
json.WriteString(nameof(key.EncryptedKey), Convert.ToBase64String(key.EncryptedKey));
Expand Down Expand Up @@ -102,7 +102,7 @@ private static void ReadPropertyValue(EncryptionData data, JsonProperty property
}
else if (property.NameEquals(nameof(data.WrappedContentKey)))
{
var key = new WrappedKey();
var key = new KeyEnvelope();
foreach (var subProperty in property.Value.EnumerateObject())
{
ReadPropertyValue(key, subProperty);
Expand Down Expand Up @@ -133,7 +133,7 @@ private static void ReadPropertyValue(EncryptionData data, JsonProperty property
}
}

private static void ReadPropertyValue(WrappedKey key, JsonProperty property)
private static void ReadPropertyValue(KeyEnvelope key, JsonProperty property)
{
if (property.NameEquals(nameof(key.Algorithm)))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ namespace Azure.Storage.Cryptography.Models
/// <summary>
/// Represents the envelope key details stored on the service.
/// </summary>
internal class WrappedKey
internal class KeyEnvelope
{
/// <summary>
/// The key identifier string.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public static ClientSideEncryptionOptions Clone(this ClientSideEncryptionOptions
{
KeyEncryptionKey = other.KeyEncryptionKey,
KeyResolver = other.KeyResolver,
KeyWrapAlgorithm = other.KeyWrapAlgorithm
KeyWrapAlgorithm = other.KeyWrapAlgorithm,
};

/// <summary>
Expand Down Expand Up @@ -63,6 +63,10 @@ public static byte[] CreateKey(int numBits)
/// </param>
/// <param name="async">Whether to perform this function asynchronously.</param>
/// <param name="cancellationToken"></param>
/// <returns>
/// Decrypted plaintext. If key could not be resolved, returns null.
/// </returns>
/// <exception cref="ClientSideEncryptionKeyNotFoundException">When key ID cannot be resolved.</exception>
public static async Task<Stream> DecryptInternal(
Stream ciphertext,
EncryptionData encryptionData,
Expand All @@ -73,6 +77,13 @@ public static async Task<Stream> DecryptInternal(
bool async,
CancellationToken cancellationToken)
{
var contentEncryptionKey = await GetContentEncryptionKeyOrDefaultAsync(
encryptionData,
keyResolver,
potentialCachedKeyWrapper,
async,
cancellationToken).ConfigureAwait(false);

Stream plaintext;
//int read = 0;
if (encryptionData != default)
Expand All @@ -96,8 +107,6 @@ public static async Task<Stream> DecryptInternal(
//read = IV.Length;
}

var contentEncryptionKey = await GetContentEncryptionKeyAsync(encryptionData, keyResolver, potentialCachedKeyWrapper, async, cancellationToken).ConfigureAwait(false);

plaintext = WrapStream(
ciphertext,
contentEncryptionKey.ToArray(),
Expand All @@ -124,16 +133,19 @@ public static async Task<Stream> DecryptInternal(
/// <param name="potentiallyCachedKeyWrapper"></param>
/// <param name="async">Whether to perform asynchronously.</param>
/// <param name="cancellationToken"></param>
/// <returns>Encryption key as a byte array.</returns>
private static async Task<Memory<byte>> GetContentEncryptionKeyAsync(
/// <returns>
/// Encryption key as a byte array.
/// </returns>
/// <exception cref="ClientSideEncryptionKeyNotFoundException">When key ID cannot be resolved.</exception>
private static async Task<Memory<byte>> GetContentEncryptionKeyOrDefaultAsync(
#pragma warning restore CS1587 // XML comment is not placed on a valid language element
EncryptionData encryptionData,
IKeyEncryptionKeyResolver keyResolver,
IKeyEncryptionKey potentiallyCachedKeyWrapper,
bool async,
CancellationToken cancellationToken)
{
IKeyEncryptionKey key;
IKeyEncryptionKey key = default;

// If we already have a local key and it is the correct one, use that.
if (encryptionData.WrappedContentKey.KeyId == potentiallyCachedKeyWrapper?.KeyId)
Expand All @@ -147,14 +159,10 @@ private static async Task<Memory<byte>> GetContentEncryptionKeyAsync(
? await keyResolver.ResolveAsync(encryptionData.WrappedContentKey.KeyId, cancellationToken).ConfigureAwait(false)
: keyResolver.Resolve(encryptionData.WrappedContentKey.KeyId, cancellationToken);
}
else
{
throw EncryptionErrors.KeyNotFound(encryptionData.WrappedContentKey.KeyId);
}

if (key == default)
{
throw EncryptionErrors.NoKeyAccessor();
throw EncryptionErrors.KeyNotFound(encryptionData.WrappedContentKey.KeyId);
}

return async
Expand Down
Loading