Skip to content

Commit f993f9d

Browse files
Client-side Encryption Added Test Coverage (Azure#24797)
* initial extra test * Forced-multipart upload/download encryption tests * undid formatting change * added stress test (ignored) Co-authored-by: jschrepp-MSFT <41338290+jschrepp-MSFT@users.noreply.github.com>
1 parent 7246335 commit f993f9d

File tree

1 file changed

+180
-20
lines changed

1 file changed

+180
-20
lines changed

sdk/storage/Azure.Storage.Blobs/tests/ClientSideEncryptionTests.cs

Lines changed: 180 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,51 @@ private static byte[] Xor(byte[] a, byte[] b)
218218
return result;
219219
}
220220

221+
/// <summary>
222+
/// Get Content Encryption Key and IV from uploaded/encrypted blob to determine expected ciphertext.
223+
/// </summary>
224+
/// <param name="plaintext">
225+
/// Data to encrypt.
226+
/// </param>
227+
/// <param name="properties">
228+
/// BlobProperties containing the wrapped CEK and IV to use for replicating encryption steps.
229+
/// </param>
230+
/// <param name="keyEncryptionKey">
231+
/// KEK used to unwrap the CEK in <paramref name="properties"/>.
232+
/// </param>
233+
/// <returns>Expected encrypted data with the given CEK and IV.</returns>
234+
private async Task<byte[]> ReplicateEncryption(byte[] plaintext, BlobProperties properties, IKeyEncryptionKey keyEncryptionKey)
235+
{
236+
// encrypt original data manually for comparison
237+
if (!properties.Metadata.TryGetValue(Constants.ClientSideEncryption.EncryptionDataKey, out string serialEncryptionData))
238+
{
239+
Assert.Fail("No encryption metadata present.");
240+
}
241+
EncryptionData encryptionMetadata = EncryptionDataSerializer.Deserialize(serialEncryptionData);
242+
Assert.NotNull(encryptionMetadata, "Never encrypted data.");
243+
244+
var explicitlyUnwrappedKey = IsAsync // can't instrument this
245+
? await keyEncryptionKey.UnwrapKeyAsync(s_algorithmName, encryptionMetadata.WrappedContentKey.EncryptedKey, s_cancellationToken).ConfigureAwait(false)
246+
: keyEncryptionKey.UnwrapKey(s_algorithmName, encryptionMetadata.WrappedContentKey.EncryptedKey, s_cancellationToken);
247+
248+
return EncryptData(
249+
plaintext,
250+
explicitlyUnwrappedKey,
251+
encryptionMetadata.ContentEncryptionIV);
252+
}
253+
254+
/// <summary>
255+
/// Download a blob without decrypting it.
256+
/// </summary>
257+
/// <param name="blob">Encrypted blob to download.</param>
258+
/// <returns>Ciphertext.</returns>
259+
private async Task<byte[]> DownloadBypassDecryption(BlobClient blob)
260+
{
261+
var encryptedDataStream = new MemoryStream();
262+
await InstrumentClient(new BlobClient(blob.Uri, Tenants.GetNewSharedKeyCredentials())).DownloadToAsync(encryptedDataStream, cancellationToken: s_cancellationToken);
263+
return encryptedDataStream.ToArray();
264+
}
265+
221266
[Test]
222267
[LiveOnly]
223268
public void CanSwapKey()
@@ -259,7 +304,7 @@ public void CanSwapKey()
259304
[LiveOnly] // cannot seed content encryption key
260305
public async Task UploadAsync(long dataSize)
261306
{
262-
var data = GetRandomBuffer(dataSize);
307+
var plaintext = GetRandomBuffer(dataSize);
263308
var mockKey = GetIKeyEncryptionKey().Object;
264309
await using (var disposable = await GetTestContainerEncryptionAsync(
265310
new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
@@ -272,28 +317,53 @@ public async Task UploadAsync(long dataSize)
272317
var blob = InstrumentClient(disposable.Container.GetBlobClient(blobName));
273318

274319
// upload with encryption
275-
await blob.UploadAsync(new MemoryStream(data), cancellationToken: s_cancellationToken);
320+
await blob.UploadAsync(new MemoryStream(plaintext), cancellationToken: s_cancellationToken);
276321

277-
// download without decrypting
278-
var encryptedDataStream = new MemoryStream();
279-
await InstrumentClient(new BlobClient(blob.Uri, Tenants.GetNewSharedKeyCredentials())).DownloadToAsync(encryptedDataStream, cancellationToken: s_cancellationToken);
280-
var encryptedData = encryptedDataStream.ToArray();
322+
var encryptedData = await DownloadBypassDecryption(blob);
323+
byte[] expectedEncryptedData = await ReplicateEncryption(plaintext, await blob.GetPropertiesAsync(), mockKey);
281324

282-
// encrypt original data manually for comparison
283-
if (!(await blob.GetPropertiesAsync()).Value.Metadata.TryGetValue(Constants.ClientSideEncryption.EncryptionDataKey, out string serialEncryptionData))
325+
// compare data
326+
Assert.AreEqual(expectedEncryptedData, encryptedData);
327+
}
328+
}
329+
330+
[TestCase(1)]
331+
[TestCase(2)]
332+
[TestCase(4)]
333+
[TestCase(8)]
334+
[LiveOnly] // cannot seed content encryption key
335+
public async Task UploadAsyncSplit(int concurrency)
336+
{
337+
int blockSize = Constants.KB;
338+
int dataSize = 16 * Constants.KB;
339+
var plaintext = GetRandomBuffer(dataSize);
340+
var mockKey = GetIKeyEncryptionKey().Object;
341+
await using (var disposable = await GetTestContainerEncryptionAsync(
342+
new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
284343
{
285-
Assert.Fail("No encryption metadata present.");
286-
}
287-
EncryptionData encryptionMetadata = EncryptionDataSerializer.Deserialize(serialEncryptionData);
288-
Assert.NotNull(encryptionMetadata, "Never encrypted data.");
344+
KeyEncryptionKey = mockKey,
345+
KeyWrapAlgorithm = s_algorithmName
346+
}))
347+
{
348+
var blobName = GetNewBlobName();
349+
var blob = InstrumentClient(disposable.Container.GetBlobClient(blobName));
350+
351+
// upload with encryption
352+
await blob.UploadAsync(
353+
new MemoryStream(plaintext),
354+
new BlobUploadOptions
355+
{
356+
TransferOptions = new StorageTransferOptions
357+
{
358+
InitialTransferSize = blockSize,
359+
MaximumTransferSize = blockSize,
360+
MaximumConcurrency = concurrency
361+
}
362+
},
363+
cancellationToken: s_cancellationToken);
289364

290-
var explicitlyUnwrappedKey = IsAsync // can't instrument this
291-
? await mockKey.UnwrapKeyAsync(s_algorithmName, encryptionMetadata.WrappedContentKey.EncryptedKey, s_cancellationToken).ConfigureAwait(false)
292-
: mockKey.UnwrapKey(s_algorithmName, encryptionMetadata.WrappedContentKey.EncryptedKey, s_cancellationToken);
293-
byte[] expectedEncryptedData = EncryptData(
294-
data,
295-
explicitlyUnwrappedKey,
296-
encryptionMetadata.ContentEncryptionIV);
365+
var encryptedData = await DownloadBypassDecryption(blob);
366+
byte[] expectedEncryptedData = await ReplicateEncryption(plaintext, await blob.GetPropertiesAsync(), mockKey);
297367

298368
// compare data
299369
Assert.AreEqual(expectedEncryptedData, encryptedData);
@@ -340,6 +410,54 @@ await blob.DownloadToAsync(stream,
340410
}
341411
}
342412

413+
[TestCase(1)]
414+
[TestCase(2)]
415+
[TestCase(4)]
416+
[TestCase(8)]
417+
[LiveOnly] // cannot seed content encryption key
418+
public async Task RoundtripSplitAsync(int concurrency)
419+
{
420+
int blockSize = Constants.KB;
421+
int dataSize = 16 * Constants.KB;
422+
423+
var data = GetRandomBuffer(dataSize);
424+
var mockKey = GetIKeyEncryptionKey();
425+
var mockKeyResolver = GetIKeyEncryptionKeyResolver(mockKey.Object).Object;
426+
var transferOptions = new StorageTransferOptions
427+
{
428+
InitialTransferSize = blockSize,
429+
MaximumTransferSize = blockSize,
430+
MaximumConcurrency = concurrency
431+
};
432+
await using (var disposable = await GetTestContainerEncryptionAsync(
433+
new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
434+
{
435+
KeyEncryptionKey = mockKey.Object,
436+
KeyResolver = mockKeyResolver,
437+
KeyWrapAlgorithm = s_algorithmName
438+
}))
439+
{
440+
var blob = InstrumentClient(disposable.Container.GetBlobClient(GetNewBlobName()));
441+
442+
// upload with encryption
443+
await blob.UploadAsync(new MemoryStream(data), transferOptions: transferOptions, cancellationToken: s_cancellationToken);
444+
445+
// download with decryption
446+
byte[] downloadData;
447+
using (var stream = new MemoryStream())
448+
{
449+
await blob.DownloadToAsync(stream,
450+
transferOptions: transferOptions,
451+
cancellationToken: s_cancellationToken);
452+
downloadData = stream.ToArray();
453+
}
454+
455+
// compare data
456+
Assert.AreEqual(data, downloadData);
457+
VerifyUnwrappedKeyWasCached(mockKey);
458+
}
459+
}
460+
343461
[TestCase(Constants.MB, 64*Constants.KB)]
344462
[TestCase(Constants.MB, Constants.MB)]
345463
[TestCase(Constants.MB, 4*Constants.MB)]
@@ -711,7 +829,7 @@ public async Task AppropriateRangeDownloadOnPlaintext(int rangeOffset, int? rang
711829
[Test]
712830
[LiveOnly] // cannot seed content encryption key
713831
[Ignore("stress test")]
714-
public async Task StressAsync()
832+
public async Task StressManyBlobsAsync()
715833
{
716834
static async Task<byte[]> RoundTripData(BlobClient client, byte[] data)
717835
{
@@ -755,6 +873,48 @@ static async Task<byte[]> RoundTripData(BlobClient client, byte[] data)
755873
}
756874
}
757875

876+
[Test]
877+
[LiveOnly] // cannot seed content encryption key
878+
[Ignore("stress test")]
879+
public async Task StressLargeBlobAsync()
880+
{
881+
const int dataSize = 100 * Constants.MB;
882+
const int blockSize = 8 * Constants.MB;
883+
884+
var data = GetRandomBuffer(dataSize);
885+
var mockKey = GetIKeyEncryptionKey().Object;
886+
var mockKeyResolver = GetIKeyEncryptionKeyResolver(mockKey).Object;
887+
var transferOptions = new StorageTransferOptions
888+
{
889+
InitialTransferSize = blockSize,
890+
MaximumTransferSize = blockSize,
891+
MaximumConcurrency = 100
892+
};
893+
await using (var disposable = await GetTestContainerEncryptionAsync(
894+
new ClientSideEncryptionOptions(ClientSideEncryptionVersion.V1_0)
895+
{
896+
KeyEncryptionKey = mockKey,
897+
KeyResolver = mockKeyResolver,
898+
KeyWrapAlgorithm = s_algorithmName
899+
}))
900+
{
901+
var client = disposable.Container.GetBlobClient(GetNewBlobName());
902+
using (var dataStream = new MemoryStream(data))
903+
{
904+
await client.UploadAsync(dataStream, transferOptions: transferOptions, cancellationToken: s_cancellationToken);
905+
}
906+
907+
byte[] downloadResult;
908+
using (var downloadStream = new MemoryStream())
909+
{
910+
await client.DownloadToAsync(downloadStream, transferOptions: transferOptions, cancellationToken: s_cancellationToken);
911+
downloadResult = downloadStream.ToArray();
912+
}
913+
914+
Assert.AreEqual(data, downloadResult);
915+
}
916+
}
917+
758918
[Test]
759919
[LiveOnly]
760920
public async Task EncryptedReuploadSuccess()

0 commit comments

Comments
 (0)