@@ -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