Skip to content

Commit

Permalink
Client-side encryption metadata deserialization case-insensitive (#30621
Browse files Browse the repository at this point in the history
)
  • Loading branch information
jaschrep-msft authored and seanmcc-msft committed Aug 22, 2022
1 parent dd17f33 commit f982e7c
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 13 deletions.
3 changes: 3 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Release History

## 12.13.1 (Unreleased)
- Added support for downloading blobs with bugged client-side encryption metadata from previous library versions.

## 12.13.0 (2022-07-07)
- Includes all features from 12.13.0-beta.1.

Expand Down
42 changes: 42 additions & 0 deletions sdk/storage/Azure.Storage.Blobs/tests/ClientSideEncryptionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.Cryptography;
Expand Down Expand Up @@ -890,6 +891,47 @@ public async Task Track1DownloadTrack2Blob()
}
}

/// <summary>
/// Track 1 had a bug where it used default casing settings for serializing properties,
/// which the application writer could change. So there exists encryption metadata in
/// Storage we do not know the casing of. We need to be able to deserialize those objects.
/// </summary>
[Test]
[LiveOnly]
public async Task DownloadBadCasing()
{
var data = new BinaryData(GetRandomBuffer(Constants.KB)); // ensure we have enough room in original data

var keyEncryptionKeyBytes = this.GenerateKeyBytes();
var keyId = this.GenerateKeyId();

var mockKey = this.GetIKeyEncryptionKey(s_cancellationToken, keyEncryptionKeyBytes, keyId).Object;
await using var disposable = await GetTestContainerEncryptionAsync(
#pragma warning disable CS0618 // obsolete
new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
{
KeyEncryptionKey = mockKey,
KeyWrapAlgorithm = s_algorithmName
});

var blob = InstrumentClient(disposable.Container.GetBlobClient(GetNewBlobName()));
await blob.UploadAsync(data.ToStream(), cancellationToken: s_cancellationToken);

// tamper metadata json key casing
Assert.IsTrue((await blob.GetPropertiesAsync()).Value.Metadata.TryGetValue(EncryptionDataKey, out string rawEncryptionData));
// pattern to match restated without string literal escapes: /"(\w+)"\s*:/
// matches json property key and captures the text inside the quotations
// (regex not perfect but will capture in our scenario)
// replaces captured key with ToUpper of said key, ensuring casing is unexpected but we can parse it anyway on download
rawEncryptionData = Regex.Replace(rawEncryptionData, "\"(\\w+)\"\\s*:", match => match.Value.ToUpper());
await blob.SetMetadataAsync(new Dictionary<string, string>
{
{ EncryptionDataKey, rawEncryptionData }
});

Assert.DoesNotThrowAsync(async () => await blob.DownloadContentAsync(cancellationToken: s_cancellationToken));
}

[Test]
[LiveOnly] // need access to keyvault service && cannot seed content encryption key
public async Task RoundtripWithKeyvaultProvider([ValueSource("GetEncryptionVersions")] ClientSideEncryptionVersion version)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,11 +145,11 @@ public static EncryptionData ReadEncryptionData(JsonElement root)

private static void ReadPropertyValue(EncryptionData data, JsonProperty property)
{
if (property.NameEquals(nameof(data.EncryptionMode)))
if (property.Name.Equals(nameof(data.EncryptionMode), StringComparison.InvariantCultureIgnoreCase))
{
data.EncryptionMode = property.Value.GetString();
}
else if (property.NameEquals(nameof(data.WrappedContentKey)))
else if (property.Name.Equals(nameof(data.WrappedContentKey), StringComparison.InvariantCultureIgnoreCase))
{
var key = new KeyEnvelope();
foreach (var subProperty in property.Value.EnumerateObject())
Expand All @@ -158,7 +158,7 @@ private static void ReadPropertyValue(EncryptionData data, JsonProperty property
}
data.WrappedContentKey = key;
}
else if (property.NameEquals(nameof(data.EncryptionAgent)))
else if (property.Name.Equals(nameof(data.EncryptionAgent), StringComparison.InvariantCultureIgnoreCase))
{
var agent = new EncryptionAgent();
foreach (var subProperty in property.Value.EnumerateObject())
Expand All @@ -167,11 +167,11 @@ private static void ReadPropertyValue(EncryptionData data, JsonProperty property
}
data.EncryptionAgent = agent;
}
else if (property.NameEquals(nameof(data.ContentEncryptionIV)))
else if (property.Name.Equals(nameof(data.ContentEncryptionIV), StringComparison.InvariantCultureIgnoreCase))
{
data.ContentEncryptionIV = Convert.FromBase64String(property.Value.GetString());
}
else if (property.NameEquals(nameof(data.KeyWrappingMetadata)))
else if (property.Name.Equals(nameof(data.KeyWrappingMetadata), StringComparison.InvariantCultureIgnoreCase))
{
var metadata = new Dictionary<string, string>();
foreach (var entry in property.Value.EnumerateObject())
Expand All @@ -180,7 +180,7 @@ private static void ReadPropertyValue(EncryptionData data, JsonProperty property
}
data.KeyWrappingMetadata = metadata;
}
else if (property.NameEquals(nameof(data.EncryptedRegionInfo)))
else if (property.Name.Equals(nameof(data.EncryptedRegionInfo), StringComparison.InvariantCultureIgnoreCase))
{
var info = new EncryptedRegionInfo();
foreach (var subProperty in property.Value.EnumerateObject())
Expand All @@ -193,39 +193,39 @@ private static void ReadPropertyValue(EncryptionData data, JsonProperty property

private static void ReadPropertyValue(KeyEnvelope key, JsonProperty property)
{
if (property.NameEquals(nameof(key.Algorithm)))
if (property.Name.Equals(nameof(key.Algorithm), StringComparison.InvariantCultureIgnoreCase))
{
key.Algorithm = property.Value.GetString();
}
else if (property.NameEquals(nameof(key.EncryptedKey)))
else if (property.Name.Equals(nameof(key.EncryptedKey), StringComparison.InvariantCultureIgnoreCase))
{
key.EncryptedKey = Convert.FromBase64String(property.Value.GetString());
}
else if (property.NameEquals(nameof(key.KeyId)))
else if (property.Name.Equals(nameof(key.KeyId), StringComparison.InvariantCultureIgnoreCase))
{
key.KeyId = property.Value.GetString();
}
}

private static void ReadPropertyValue(EncryptionAgent agent, JsonProperty property)
{
if (property.NameEquals(nameof(agent.EncryptionAlgorithm)))
if (property.Name.Equals(nameof(agent.EncryptionAlgorithm), StringComparison.InvariantCultureIgnoreCase))
{
agent.EncryptionAlgorithm = new ClientSideEncryptionAlgorithm(property.Value.GetString());
}
else if (property.NameEquals(EncryptionAgent_EncryptionVersionName))
else if (property.Name.Equals(EncryptionAgent_EncryptionVersionName, StringComparison.InvariantCultureIgnoreCase))
{
agent.EncryptionVersion = property.Value.GetString().ToClientSideEncryptionVersion();
}
}

private static void ReadPropertyValue(EncryptedRegionInfo info, JsonProperty property)
{
if (property.NameEquals(nameof(info.NonceLength)))
if (property.Name.Equals(nameof(info.NonceLength), StringComparison.InvariantCultureIgnoreCase))
{
info.NonceLength = property.Value.GetInt32();
}
else if (property.NameEquals(nameof(info.DataLength)))
else if (property.Name.Equals(nameof(info.DataLength), StringComparison.InvariantCultureIgnoreCase))
{
info.DataLength = property.Value.GetInt32();
}
Expand Down
3 changes: 3 additions & 0 deletions sdk/storage/Azure.Storage.Queues/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Release History

## 12.11.1 (Unreleased)
- Added support for receiving queue messages with bugged client-side encryption metadata from previous library versions.

## 12.11.0 (2022-07-07)
- Includes all features from 12.10.1-beta.1.

Expand Down

0 comments on commit f982e7c

Please sign in to comment.