Skip to content

Conversation

@jaschrep-msft
Copy link
Member

Ported client-side encryption work from long-diverged branch. Was an inheritance approach, is now part of the main package internals.
Ported over internal WindowStream from stg73base, which allows us to prematurely end streams.
Removed internal RollingBufferStream, as uploads no longer require seekable streams as input. Upload paths that handle non-seekable streams already have their own buffer strategy.

Ported blob client-side encryption work from long-diverged branch. Was an
inheritance approach, is now part of the main package internals.
Ported over WindowStream from stg73base, which allows us to prematurely
end streams.
Removed RollingBufferStream, as uploads no longer require seekable
streams as input.
@jaschrep-msft
Copy link
Member Author

jaschrep-msft commented May 19, 2020

Ported over blobs. Coming up in this PR, porting queues, extra tests for blob and queue, introduction of ThrowForMissingDecryptionKey flag.

Copy link
Contributor

@kasobol-msft kasobol-msft left a comment

Choose a reason for hiding this comment

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

The publicly facing part looks good (I left ~2 questions for that).
I think we should work little bit on cleaning up the implementation.

}
namespace Azure.Storage.Blobs.Specialized
{
public partial class AdvancedBlobClientOptions : Azure.Storage.Blobs.BlobClientOptions
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not 100% convinced Advanced is good prefix. Maybe Extended?
Or should we say Hidden? :D

Copy link
Member

Choose a reason for hiding this comment

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

I like Extended

Copy link
Member Author

Choose a reason for hiding this comment

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

I can go either way. Can we get @tg-msft's input on naming decisions?

Copy link
Member Author

Choose a reason for hiding this comment

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

I moved to Extended. We'll let final API review catch the name if it's unwanted.


#region Utility

private byte[] LocalManualEncryption(byte[] data, byte[] key, byte[] iv)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: method naming should be verb-ish. EncryptLocally or DoLocalEncryption. noun-ish terms are reserved for types. btw. what does manual mean?

Copy link
Member Author

Choose a reason for hiding this comment

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

It means I'm invoking the encryption myself, instead of going through the client's automated processes. Maybe explicit is a better word than manual?

Copy link
Member

Choose a reason for hiding this comment

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

Maybe bypass may be a candidate term to consider? Is that what you're trying to do?

Copy link
Contributor

Choose a reason for hiding this comment

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

Independent/External/Baseline?

Manual indicates human involvement in the process.

}


#region Utility
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I'm not sure what this region means. besides it's not closed.

Copy link
Member Author

Choose a reason for hiding this comment

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

It is closed, write above the first test.

Region is intended to separate tests from methods that help run the tests.

Copy link
Contributor

Choose a reason for hiding this comment

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

okay. I think vertical placement (tests near tests, aids near aids) is good enough. I'd avoid Utility naming if you want to keep region

}

/// <summary>
/// AES-CBC using a 256 bit key.
Copy link
Contributor

Choose a reason for hiding this comment

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

this class doesn't seem to be dependent on algorithm. this information may find better home.

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy/paste error. Property is necessary but comment is irrelevant.

/// </param>
/// <returns>Transformed content stream and metadata.</returns>
internal virtual (Stream, Metadata) TransformContent(Stream content, Metadata metadata)
private async Task<(Stream, Metadata)> ClientSideEncryptInternal(
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe we could keep the TransformContent idea and still inject encryption just make it all internal? that way we'd keep crypto related stuff in other class/namespace without cluttering blobclient.

Copy link
Member Author

Choose a reason for hiding this comment

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

I've pulled out all the crypto transformations, but have left in the explicit call to an encryption method instead of a generic TransformContent. Upcoming push.

// 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);
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe we could make this extension and move out crypto code from this client ?

Copy link
Member Author

Choose a reason for hiding this comment

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

Created new classes to handle the translation between clients and service-agnostic encryption/decryption logic. Upcoming push.

Queues now has a listener for when only some messages cannot be
decrypted. these messages are filtered out of the response and sent to
the listened instead. If no listener is provided, the whole fetch
throws.
Some PR comments addressed.
Copy link
Contributor

@kasobol-msft kasobol-msft left a comment

Choose a reason for hiding this comment

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

Looks good for shipping preview from it. We should address rest of the feedback here or before GA.


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


namespace Azure.Storage.Queues.Tests
{
internal class MockMissingClientSideEncryptionKeyListener : IMissingClientSideEncryptionKeyListener
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd use Moq for that as well.

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

Copy link
Member

@gapra-msft gapra-msft left a comment

Choose a reason for hiding this comment

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

Looks good to me

Copy link
Contributor

@kasobol-msft kasobol-msft left a comment

Choose a reason for hiding this comment

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

It's much easier to navigate this code now. Thank you!

Please take a look at the comment I left on QueueClient crypto extension and see if you want to do it before providing another drop.

bool async = true,
CancellationToken cancellationToken = default)
{
// TODO uncomment when upload from file gets its own implementation
Copy link
Contributor

Choose a reason for hiding this comment

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

to be addressed in this PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

No. This is to do with future work to give upload from file its own implementation separate from upload from stream. However, such an implementation will be incompatible with this feature, and so it will need to revert to upload from stream. I can clarify this.

Comment on lines 716 to 717
stream = await new BlobClientSideDecryptor(ClientSideEncryption)
.ClientSideDecryptInternal(stream, response.Value.Metadata, requestedRange, response.Value.ContentRange, async, cancellationToken).ConfigureAwait(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Two things to consider:

  • It seems that BlobClientSideDecryptor is stateless in this context. Could be a member (field) in this class instead of storing encryption options.
  • It's not far away from here to create IReadStreamTransformer and have BlobClientSideDecryptor : IReadStreamTransformer

not blocking here, but would be nice to have.

Comment on lines 953 to 954
(content, metadata) = await new BlobClientSideEncryptor(ClientSideEncryption)
.ClientSideEncryptInternal(content, metadata, async, cancellationToken).ConfigureAwait(false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Similar feedback here as for Decryptor.

Comment on lines 64 to 67
//Stream seekableCiphertext = new RollingBufferStream(
// nonSeekableCiphertext,
// EncryptionConstants.DefaultRollingBufferSize,
// GetExpectedCryptoStreamLength(originalLength));
Copy link
Contributor

Choose a reason for hiding this comment

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

Please cleanup commented out code here and there. Either we should work items for these improvements or implement them right away.

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 was old code that got moved around in the refactors. Is obsolete and will be removed.

/// <param name="async">Whether to wrap the CEK asynchronously.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>The wrapped stream to read from and the encryption metadata for the wrapped stream.</returns>
public static async Task<(Stream ciphertext, EncryptionData encryptionData)> EncryptInternal(
Copy link
Contributor

Choose a reason for hiding this comment

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

similar feedback here.

/// </summary>
/// <param name="data">Data to serialize.</param>
/// <returns>The JSON UTF8 bytes.</returns>
public static ReadOnlyMemory<byte> SerializeEncryptionData(EncryptionData data)
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 have to be public ?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's on an internal class.

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean we call it from method above. Do we call it from somewhere else? if not then we could hide it more.

@jaschrep-msft jaschrep-msft force-pushed the feature/storage/integrated-clientside-encryption branch from b281a88 to 55ab4cc Compare June 2, 2020 23:03
Copy link
Member

@tg-msft tg-msft left a comment

Choose a reason for hiding this comment

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

Commenting on the APIs first as they're the most important. I'll run through the rest as well.

public virtual Azure.Storage.Blobs.Specialized.BlobBaseClient WithSnapshot(string snapshot) { throw null; }
protected virtual Azure.Storage.Blobs.Specialized.BlobBaseClient WithSnapshotCore(string snapshot) { throw null; }
}
public static partial class BlobClientExtensions
Copy link
Member

Choose a reason for hiding this comment

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

This should go in SpecializedBlobExtensions so we don't add a new type.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll mirror this in Queues as well. There needed to be a new type no matter what, but now it will be in line with this Blobs change.

public new Azure.Storage.Blobs.Specialized.BlockBlobClient WithSnapshot(string snapshot) { throw null; }
protected sealed override Azure.Storage.Blobs.Specialized.BlobBaseClient WithSnapshotCore(string snapshot) { throw null; }
}
public partial class ExtendedBlobClientOptions : Azure.Storage.Blobs.BlobClientOptions
Copy link
Member

Choose a reason for hiding this comment

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

Extended is probably not the best prefix because a lot of folks have a visceral reaction to it. Could we call this SpecializedBlobClientOptions to match the Specialized namespace and terminology we've been building?

<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.Security.KeyVault.Keys" />
<PackageReference Include="Microsoft.Azure.KeyVault.Core" />
<PackageReference Include="Microsoft.Azure.Storage.Queue" />
Copy link
Member

Choose a reason for hiding this comment

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

Do we really want to reference the track1 version of these libraries?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes. There are compatibility tests to ensure each library can read the data encoded by the other. This is an upgrade requirement for anyone who used client-side encryption in track 1.

Copy link
Contributor

Choose a reason for hiding this comment

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

better safe than sorry. We can think about spinning extra test package(s) where we test track1 track2 interop aspects in long run or take it as follow up from here.

Copy link
Member

Choose a reason for hiding this comment

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

Yes I do think it is worth considering multiple test projects to help ensure we don't have any weird cross-pollination between the track 1 and track 2 library.

@jaschrep-msft
Copy link
Member Author

/azp run net - storage - ci

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Member

@tg-msft tg-msft left a comment

Choose a reason for hiding this comment

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

I'm adding a bunch more comments, but only things affecting the API should be worried about for Preview. The rest can be addressed in future changes if you think they're worth doing.

clientSideEncryption: default,
encryptionScope)
{
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit - might be cleaner to validate here than the in the extension method so you've mirrored the logic across all your constructors instead of splitting it between some .ctors and the extension.

Copy link
Member Author

Choose a reason for hiding this comment

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

There aren't any encryption options being passed in here. It's definitional that they don't exist. Are you suggesting we add the encryption options to the constructor explicitly even though we know we will never use them?

I know outside this comment you mentioned a complete refactoring of how our constructors are handled internally, which I mentioned is already an upcoming work item. Are you okay with this being addressed during that work?

Copy link
Member

Choose a reason for hiding this comment

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

I'm okay with anything but the API changes happening as future work.


private static void AssertNoClientSideEncryption(BlobClientOptions options)
{
if (options._clientSideEncryptionOptions != default)
Copy link
Member

Choose a reason for hiding this comment

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

Could options ever be null when you get here?

<ItemGroup>
<Compile Include="$(AzureStorageSharedSources)ClientsideEncryption\*.cs" Link="Shared\ClientsideEncryption\%(RecursiveDir)\%(Filename)%(Extension)" />
<Compile Include="$(AzureStorageSharedSources)ClientsideEncryption\Models\*.cs" Link="Shared\ClientsideEncryption\Models\%(RecursiveDir)\%(Filename)%(Extension)" />
<Compile Remove="C:\Repos\azure-sdk-for-net\sdk\storage\Azure.Storage.Common\src\Shared\ClientsideEncryption\Models\EncryptedBlobRange.cs" />
Copy link
Member

Choose a reason for hiding this comment

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

This shouldn't be checked in.

{
private const string WildcardMarker = "*";

public struct RangeUnit
Copy link
Member

Choose a reason for hiding this comment

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

Nit - nested types are always considered kind of weird in .NET. It would probably be nicer to pull this out into another internal strict and just call it ContentRangeUnit.

Copy link
Member

Choose a reason for hiding this comment

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

Nit - maybe make this a readonly struct.

/// </summary>
internal virtual ClientSideEncryptionOptions ClientSideEncryption => _clientSideEncryption;

internal bool UsingClientSideEncryption => ClientSideEncryption != default;
Copy link
Member

Choose a reason for hiding this comment

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

Nit - this feels a little gratuitous.

}
public enum ClientSideEncryptionVersion
{
V1_0 = 0,
Copy link
Member

Choose a reason for hiding this comment

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

Nit - start this enum at = 1 and then you can throw if someone passes default. Follow the same pattern we use for ServiceVersion.

/// method that will Assert.Inconclusive if the desired tenant wasn't
/// defined in this configuration.
/// </summary>
private IDictionary<string, KeyVaultConfiguration> KeyVaults { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

void OnFailure(Azure.Storage.Queues.Models.QueueMessage message, System.Exception exception);
System.Threading.Tasks.Task OnFailureAsync(Azure.Storage.Queues.Models.PeekedMessage message, System.Exception exception);
System.Threading.Tasks.Task OnFailureAsync(Azure.Storage.Queues.Models.QueueMessage message, System.Exception exception);
}
Copy link
Member

Choose a reason for hiding this comment

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

This should be an abstract class instead of an interface

Copy link
Member

Choose a reason for hiding this comment

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

I worry that we're doing something different here than we did for QuickQuery. That just took an Action.

Copy link
Member Author

Choose a reason for hiding this comment

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

If we believe we don't need async methods here then so be it. But we have two distinct types of QueueMessage and PeekedMessage that don't have a common base. Is accepting more than one action when the user probably wants to always supply both really the best option? I'm not sure of a better way to supply this functionality.

Copy link
Member

@tg-msft tg-msft Jun 4, 2020

Choose a reason for hiding this comment

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

@KrzysztofCwalina and I talked about this a bit and came up with the following to match what we did for QuickQuery a little better here:

    public class QueueClientSideEncryptionOptions : ClientSideEncryptionOptions
    {
        public event EventHandler<DecryptionFailureEventArgs> DecryptionFailed;
    }

    public class ClientSideDecryptionFailureEventArgs
    {
        public Exception Exception { get; }
        public ClientSideDecryptionFailureEventArgs(Exception exception);
    }

and you could use it like:

var options = new QueueClientSideEncryptionOptions();
options.KeyResolver = ...;
options.DecryptionFailed +=
    (object sender, ClientSideDecryptionFailureEventArgs args) =>
    {
        if (sender is PeekedMessage peek)
        {
             Console.WriteLine($"Failed to peek at {peek.MessageId}");
        }
        else if (sender is QueueMessage message)
        {
             Console.WriteLine($"Failed to receive {message.MessageId}");
        }
    };

It's only sync for now, but so is QuickQuery's error handler and I'd wait to hear feedback from folks if they need an async event.

Do you think this would work?

Copy link
Member Author

Choose a reason for hiding this comment

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

Is the suggestion that SpecializedQueueClientOptions now have a property for QueueClientSideEncryptionOptions instead of the normal ClientSideEncryptionOptions, or that we still accept the base class and work it through internals like we do with client options in the first place?

Copy link
Member

Choose a reason for hiding this comment

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

I'd say leave it as the base ClientSideEncryptionOptions in SpecializedQueueClientOptions so people could pass the same value for Blobs and Queues. You'd only need to learn about QueueClientSideEncryptionOptions if you were trying to handle failures which should be an advanced scenario.

On a related note - maybe we should change the semantics to throw an exception when you can't decrypt if there's no failure handler? Then new users who aren't trying to use multiple keys/etc. will still have an easy notification that something went wrong but advanced scenarios are still possible for special partners?

Copy link
Member

Choose a reason for hiding this comment

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

And you could just do an if (options is QueueClientSideEncryptionOptions advanced) { _failureHandler = advanced.DecryptionFailed; } instead of a bunch of internal state sharing since ClientSideEncryptionOptions is in Common.

{
void OnFailure(Azure.Storage.Queues.Models.PeekedMessage message, System.Exception exception);
void OnFailure(Azure.Storage.Queues.Models.QueueMessage message, System.Exception exception);
System.Threading.Tasks.Task OnFailureAsync(Azure.Storage.Queues.Models.PeekedMessage message, System.Exception exception);
Copy link
Member

Choose a reason for hiding this comment

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

The property name should just be DecryptionFailureListener

}
public static partial class SpecializedQueueExtensions
{
public static Azure.Storage.Queues.QueueClient WithClientSideEncryptionFailureListener(this Azure.Storage.Queues.QueueClient client, Azure.Storage.Queues.Specialized.IClientSideDecryptionFailureListener listener) { throw null; }
Copy link
Member

Choose a reason for hiding this comment

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

Is this a requirement? What's the sceanrio for needing to change the failure listener of an existing queue?

Copy link
Member Author

Choose a reason for hiding this comment

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

It seemed to go hand in hand with replacing the key. Private drops have been given with this functionality, so we'll have to check with our partners on whether they actually use this if we think it should be removed.

Copy link
Member

Choose a reason for hiding this comment

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

If we move to the EventHandler<T> approach, hopefully the single With() method will suffice.

Copy link
Member

@tg-msft tg-msft left a comment

Choose a reason for hiding this comment

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

Adding a few more comments

{
internal class EncryptedMessage
{
public string EncryptedMessageContents { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

Should this be EncryptedMessageText to map to QueueMessage.MessageText?


namespace Azure.Storage.Queues.Specialized.Models
{
internal static class EncryptedMessageSerializer
Copy link
Member

Choose a reason for hiding this comment

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

Nit - Any reason not to put these in the EncryptedMessage class?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean placing the serializer as a nested class to what it is serializing?

Copy link
Member

Choose a reason for hiding this comment

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

No, just merging the two classes together.

}
catch (Exception e) when (_listener != default)
{
if (async)
Copy link
Member

Choose a reason for hiding this comment

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

This pattern doesn't feel right. What if I implemented a sync listener becuase I only Console.WriteLine but use QueueClient.ReceiveAsync to make an async method call?

Copy link
Member Author

Choose a reason for hiding this comment

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

Will come back to this based on discussion of what form the listener should even take.

}
}
}
return filteredMessages.ToArray();
Copy link
Member

Choose a reason for hiding this comment

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

So you have to implement a listener if you don't want to wait for the visiblity timeout on messages you can't read? This design feels tricky to use correctly.

Copy link
Member Author

Choose a reason for hiding this comment

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

The alternatives as far as we've explored are to

  1. Return the serialized encrypted message schema and let the user write the code to discover they failed to decrypt.
  2. Silently filter out messages we couldn't decrypt.

I'm not happy with either of those. The first is undue burden on the customer, whereas through the listener we do the determination for them and they handle it how they wish. The second feels dangerous in that queue messages can just time out with no one noticing.

I suppose an option 3 could exist of overriding the failed messages to throw upon accessing the message text and just always return all the messages, but I'm not sure whether that's useful.

/// </summary>
internal virtual ClientSideEncryptionOptions ClientSideEncryption => _clientSideEncryption;

private readonly IClientSideDecryptionFailureListener _missingClientSideEncryptionKeyListener;
Copy link
Member

Choose a reason for hiding this comment

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

Nit - this name doesn't match the type like the other uses.

/// <summary>
/// The protocol version used for encryption.
/// </summary>
public ClientSideEncryptionVersion Protocol { get; set; }
Copy link
Member

Choose a reason for hiding this comment

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

Nit - I'd suggest naming this EncryptionVersion instead of Protocol to match the name in Options.

Copy link
Member

@tg-msft tg-msft left a comment

Choose a reason for hiding this comment

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

Adding a few last comments. I'm approving and it's fine if you want to merge this now and continue addressing the last API feedback in a follow up PR.

{
Assembly assembly = typeof(EncryptionData).Assembly;
var platformInformation = $"({RuntimeInformation.FrameworkDescription}; {RuntimeInformation.OSDescription})";
return $"azsdk-net-{assembly.GetName().Name}/{assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>().InformationalVersion} {platformInformation}";
Copy link
Member

Choose a reason for hiding this comment

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

Nit - might be worth pulling this into a shared source file for TelemetryPolicy and your code.

{
internal static class ClientSideEncryptionOptionsExtensions
{
public static ClientSideEncryptionOptions Clone(this ClientSideEncryptionOptions options)
Copy link
Member

Choose a reason for hiding this comment

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

Nit - maybe add a mainteance note in ClientSideEncryptionOptions that this msut be updated when changes are introduced there?

Copy link
Member

@tg-msft tg-msft left a comment

Choose a reason for hiding this comment

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

There's one small thing we should change about the event source, but it otherwise looks great to me. :shipit:


internal void OnDecryptionFailed(object message, Exception e)
{
DecryptionFailed?.Invoke(this, new ClientSideDecryptionFailureEventArgs(message, e));
Copy link
Member

Choose a reason for hiding this comment

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

We should pass message as the source instead of this (i.e., it was the message that failed to decrypt). This also kind of hides the wart of dealing with QueueMessage vs PeekedMessage behind the event handler pattern that forces object sender.

/// Message the failure occured with. Can be an instance of either
/// <see cref="Queues.Models.QueueMessage"/> or <see cref="Queues.Models.PeekedMessage"/>.
/// </summary>
public object Message { get; }
Copy link
Member

Choose a reason for hiding this comment

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

This should be removed as well if we're passing it as the event's sender.

Comment on lines 43 to 79
private QueueClientSideEncryptionOptions CloneAsQueueClientSideEncryptionOptions()
{
// clone base class but as instance of this class
var newOptions = new QueueClientSideEncryptionOptions(EncryptionVersion);
ClientSideEncryptionOptionsExtensions.CopyOptions(this, newOptions);

// clone data specific to this subclass
newOptions.DecryptionFailed = DecryptionFailed;

return newOptions;
}

/// <summary>
/// Clones the given <see cref="ClientSideEncryptionOptions"/> as an instance of
/// <see cref="QueueClientSideEncryptionOptions"/>. If the given instance is also a
/// <see cref="QueueClientSideEncryptionOptions"/>, this clones it's specialty data as well.
/// </summary>
/// <returns></returns>
internal static QueueClientSideEncryptionOptions CloneFrom(ClientSideEncryptionOptions options)
{
if (options == default)
{
return default;
}
else if (options is QueueClientSideEncryptionOptions)
{
return ((QueueClientSideEncryptionOptions)options).CloneAsQueueClientSideEncryptionOptions();
}
else
{
// clone base class but as instance of this class
var newOptions = new QueueClientSideEncryptionOptions(options.EncryptionVersion);
ClientSideEncryptionOptionsExtensions.CopyOptions(options, newOptions);

return newOptions;
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Nit - could we simplify both methods to just?

internal static QueueClientSideEncryptionOptions CloneFrom(ClientSideEncryptionOptions options)
{
    if (options == default) { return default; }
    var newOptions = new QueueClientSideEncryptionOptions(options.EncryptionVersion);
    ClientSideEncryptionOptionsExtensions.CopyOptions(options, newOptions);
    if (options is QueueClientSideEncryptionOptions queueOptions)
    {
        newOptions.DecryptionFailed = queueOptions.DecryptionFailed;
	}
    return newOptions;
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Huh. Somehow that gets past the compiler restrictions on when an event can be assigned. I did something nearly identical before and it said no. Will fix.

@jaschrep-msft
Copy link
Member Author

/azp run net - storage - ci

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@tg-msft
Copy link
Member

tg-msft commented Jun 5, 2020

/check-enforcer reset

@tg-msft
Copy link
Member

tg-msft commented Jun 5, 2020

/check-enforcer evaluate

@jaschrep-msft jaschrep-msft merged commit 639e185 into Azure:master Jun 8, 2020
@jaschrep-msft jaschrep-msft deleted the feature/storage/integrated-clientside-encryption branch August 3, 2022 15:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants