From 6defb2714ae43179c66eb5216c03f5f315bb6ca2 Mon Sep 17 00:00:00 2001 From: asorrin-msft Date: Thu, 24 Mar 2016 12:08:00 -0700 Subject: [PATCH 1/2] Block blob upload with encryption now able to do PutBlob, not just Put Block + Put Block List. --- ChangeLog.txt | 3 +- .../blob/CloudBlobEncryptionTests.java | 115 ++++++++++++++++++ .../azure/storage/blob/CloudBlockBlob.java | 63 +++++++--- .../microsoft/azure/storage/core/Utility.java | 77 ++++++++++++ 4 files changed, 243 insertions(+), 15 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index 20514235d96f5..46eeaca40a4d0 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,5 +1,6 @@ 2015.10.05 Version 4.0-alpha-1 - * Added preview support for client side encryption for blobs, queues and tables. + * Added support for client side encryption for blobs, queues and tables. + * Since the encryption preview, added functionality where uploading encrypted blobs can be done with just PutBlob, not PutBlock + PutBlockList, if the blob is small enough. 2015.10.05 Version 4.0.0 * Removed deprecated table AtomPub support. diff --git a/microsoft-azure-storage-test/src/com/microsoft/azure/storage/blob/CloudBlobEncryptionTests.java b/microsoft-azure-storage-test/src/com/microsoft/azure/storage/blob/CloudBlobEncryptionTests.java index e6d7915ca0bd3..e1ce4e4e4edf4 100644 --- a/microsoft-azure-storage-test/src/com/microsoft/azure/storage/blob/CloudBlobEncryptionTests.java +++ b/microsoft-azure-storage-test/src/com/microsoft/azure/storage/blob/CloudBlobEncryptionTests.java @@ -36,6 +36,7 @@ import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.lang.mutable.MutableInt; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -47,6 +48,8 @@ import com.microsoft.azure.storage.Constants; import com.microsoft.azure.storage.DictionaryKeyResolver; import com.microsoft.azure.storage.OperationContext; +import com.microsoft.azure.storage.SendingRequestEvent; +import com.microsoft.azure.storage.StorageEvent; import com.microsoft.azure.storage.StorageException; import com.microsoft.azure.storage.TestHelper; import com.microsoft.azure.storage.TestRunners.CloudTests; @@ -895,4 +898,116 @@ public void testBlobEncryptionWithStrictModeOnPartialBlob() throws URISyntaxExce assertEquals(ex.getMessage(), SR.ENCRYPTION_NOT_SUPPORTED_FOR_OPERATION); } } + + @Test + public void testBlockBlobEncryptionCountOperationsEncryptCalculateMD5PassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(true, true, true); + } + + @Test + public void testBlockBlobEncryptionCountOperationsEncryptCalculateMD5NoPassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(true, true, false); + } + + @Test + public void testBlockBlobEncryptionCountOperationsEncryptNoCalculateMD5PassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(true, false, true); + } + + @Test + public void testBlockBlobEncryptionCountOperationsEncryptNoCalculateMD5NoPassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(true, false, false); + } + + @Test + public void testBlockBlobEncryptionCountOperationsNoEncryptCalculateMD5PassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(false, true, true); + } + + @Test + public void testBlockBlobEncryptionCountOperationsNoEncryptCalculateMD5NoPassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(false, true, false); + } + + @Test + public void testBlockBlobEncryptionCountOperationsNoEncryptNoCalculateMD5PassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(false, false, true); + } + + @Test + public void testBlockBlobEncryptionCountOperationsNoEncryptNoCalculateMD5NoPassInLength() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.runBlockBlobEncryptionTests(false, false, false); + } + + public void runBlockBlobEncryptionTests(boolean encryptData, boolean calculateMD5, boolean passInLength) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException, IOException { + this.doEncryptionTestCountOperations(0, 1, encryptData, calculateMD5, passInLength); // Test the zero-byte case + this.doEncryptionTestCountOperations(10, 1, encryptData, calculateMD5, passInLength); // Test a case that should definitely fit in one put blob, and is not 16-byte aligned. + this.doEncryptionTestCountOperations(1 * Constants.MB, 1, encryptData, calculateMD5, passInLength); // Test a case that is 16-byte aligned, and should fit in one put blob + this.doEncryptionTestCountOperations(13 * Constants.MB, 5, encryptData, calculateMD5, passInLength); // Test a case that should not hit put blob, but instead several put block + put block list. + } + + private void doEncryptionTestCountOperations(int size, int count, boolean encryptData, boolean calculateMD5, boolean passInLength) throws URISyntaxException, StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, IOException + { + byte[] buffer = BlobTestHelper.getRandomBuffer(size); + + CloudBlockBlob blob = this.container.getBlockBlobReference("blockblob"); + + // Create the Key to be used for wrapping. + SymmetricKey aesKey = TestHelper.getSymmetricKey(); + + // Create the resolver to be used for unwrapping. + DictionaryKeyResolver resolver = null; + + // Set the encryption policy on the request options. + BlobRequestOptions uploadOptions = new BlobRequestOptions(); + if (encryptData) { + resolver = new DictionaryKeyResolver(); + resolver.add(aesKey); + + // Create the encryption policy to be used for upload. + BlobEncryptionPolicy uploadPolicy = new BlobEncryptionPolicy(aesKey, null); + uploadOptions.setEncryptionPolicy(uploadPolicy); + } + + uploadOptions.setStoreBlobContentMD5(calculateMD5); + uploadOptions.setUseTransactionalContentMD5(calculateMD5); + uploadOptions.setDisableContentMD5Validation(!calculateMD5); + uploadOptions.setSingleBlobPutThresholdInBytes(8 * Constants.MB); + blob.setStreamWriteSizeInBytes(4 * Constants.MB); + + OperationContext opContext = new OperationContext(); + + final MutableInt operationCount = new MutableInt(0); + + opContext.getSendingRequestEventHandler().addListener(new StorageEvent() { + + @Override + public void eventOccurred(SendingRequestEvent eventArg) { + operationCount.increment(); + } + }); + + // Upload the encrypted contents to the blob. + ByteArrayInputStream stream = new ByteArrayInputStream(buffer); + blob.upload(stream, passInLength ? size : -1, null, uploadOptions, opContext); + assertEquals(operationCount.intValue(), count); + + // Set the decryption policy on the request options. + BlobRequestOptions downloadOptions = new BlobRequestOptions(); + if (encryptData) { + // Download the encrypted blob. + // Create the decryption policy to be used for download. There is no need to specify the + // key when the policy is only going to be used for downloads. Resolver is sufficient. + BlobEncryptionPolicy downloadPolicy = new BlobEncryptionPolicy(null, resolver); + downloadOptions.setEncryptionPolicy(downloadPolicy); + } + + // Download and decrypt the encrypted contents from the blob. + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + blob.download(outputStream, null, downloadOptions, null); + + // Compare that the decrypted contents match the input data. + TestHelper.assertStreamsAreEqualAtIndex(stream, new ByteArrayInputStream(outputStream.toByteArray()), 0, 0, + size, 2 * 1024); + } } diff --git a/microsoft-azure-storage/src/com/microsoft/azure/storage/blob/CloudBlockBlob.java b/microsoft-azure-storage/src/com/microsoft/azure/storage/blob/CloudBlockBlob.java index bba6ccab26845..d4e1fce4fa5ba 100644 --- a/microsoft-azure-storage/src/com/microsoft/azure/storage/blob/CloudBlockBlob.java +++ b/microsoft-azure-storage/src/com/microsoft/azure/storage/blob/CloudBlockBlob.java @@ -643,43 +643,78 @@ public void upload(final InputStream sourceStream, final long length, final Acce StreamMd5AndLength descriptor = new StreamMd5AndLength(); descriptor.setLength(length); + + InputStream inputDataStream = sourceStream; + + // Initial check - skip the PutBlob operation if the input stream isn't markable, or if the length is known to + // be greater than the threshold. + boolean skipPutBlob = !inputDataStream.markSupported() || descriptor.getLength() > options.getSingleBlobPutThresholdInBytes(); - if (sourceStream.markSupported()) { + if (inputDataStream.markSupported()) { // Mark sourceStream for current position. - sourceStream.mark(Constants.MAX_MARK_LENGTH); + inputDataStream.mark(Constants.MAX_MARK_LENGTH); } - // If the stream is rewindable and the length is unknown or we need to + // If we're not yet skipping PutBlob and we need to encrypt, encrypt the data and check that the encrypted + // data is under the threshold. + // Note this will abort at + // options.getSingleBlobPutThresholdInBytes() bytes and return -1. + if (!skipPutBlob && options.getEncryptionPolicy() != null) + { + class GettableByteArrayOutputStream extends ByteArrayOutputStream { + public byte[] getByteArray() { return this.buf; } + } + + Cipher cipher = options.getEncryptionPolicy().createAndSetEncryptionContext(this.getMetadata(), false /* noPadding */); + GettableByteArrayOutputStream targetStream = new GettableByteArrayOutputStream(); + long byteCount = Utility.encryptStreamIfUnderThreshold(inputDataStream, targetStream, cipher, descriptor.getLength(), + options.getSingleBlobPutThresholdInBytes() + 1 /*abandon if the operation hits this limit*/); + + if (byteCount >= 0) + { + inputDataStream = new ByteArrayInputStream(targetStream.getByteArray()); + descriptor.setLength(byteCount); + } + else + { + // If the encrypted data is over the threshold, skip PutBlob. + skipPutBlob = true; + } + } + + // If we're not yet skipping PutBlob, and the length is still unknown or we need to // set md5, then analyze the stream. // Note this read will abort at // options.getSingleBlobPutThresholdInBytes() bytes and return // -1 as length in which case we will revert to using a stream as it is // over the single put threshold. - if (sourceStream.markSupported() - && (length < 0 || (options.getStoreBlobContentMD5() && length <= options - .getSingleBlobPutThresholdInBytes()))) { + if (!skipPutBlob && (descriptor.getLength() < 0 || options.getStoreBlobContentMD5())) { // If the stream is of unknown length or we need to calculate // the MD5, then we we need to read the stream contents first - descriptor = Utility.analyzeStream(sourceStream, length, options.getSingleBlobPutThresholdInBytes() + 1, + descriptor = Utility.analyzeStream(inputDataStream, descriptor.getLength(), + options.getSingleBlobPutThresholdInBytes() + 1 /*abandon if the operation hits this limit*/, true /* rewindSourceStream */, options.getStoreBlobContentMD5()); if (descriptor.getMd5() != null && options.getStoreBlobContentMD5()) { this.properties.setContentMD5(descriptor.getMd5()); } + + // If the data is over the threshold, skip PutBlob. + if (descriptor.getLength() == -1 || descriptor.getLength() > options.getSingleBlobPutThresholdInBytes()) + { + skipPutBlob = true; + } } - // If the stream is rewindable, and the length is known and less than - // threshold the upload in a single put, otherwise use a stream. - if (sourceStream.markSupported() && descriptor.getLength() != -1 - && descriptor.getLength() < options.getSingleBlobPutThresholdInBytes() + 1 - && options.getEncryptionPolicy() == null) { - this.uploadFullBlob(sourceStream, descriptor.getLength(), accessCondition, options, opContext); + // By now, the skipPutBlob is completely correct. + if (!skipPutBlob) { + this.uploadFullBlob(inputDataStream, descriptor.getLength(), accessCondition, options, opContext); } else { final BlobOutputStream writeStream = this.openOutputStream(accessCondition, options, opContext); try { - writeStream.write(sourceStream, length); + writeStream.write(inputDataStream, length); } finally { writeStream.close(); diff --git a/microsoft-azure-storage/src/com/microsoft/azure/storage/core/Utility.java b/microsoft-azure-storage/src/com/microsoft/azure/storage/core/Utility.java index a10b168126312..919c04bd869f2 100644 --- a/microsoft-azure-storage/src/com/microsoft/azure/storage/core/Utility.java +++ b/microsoft-azure-storage/src/com/microsoft/azure/storage/core/Utility.java @@ -14,6 +14,7 @@ */ package com.microsoft.azure.storage.core; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -40,6 +41,8 @@ import java.util.TimeZone; import java.util.concurrent.TimeoutException; +import javax.crypto.Cipher; +import javax.crypto.CipherOutputStream; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; @@ -226,6 +229,80 @@ public static StreamMd5AndLength analyzeStream(final InputStream sourceStream, l return retVal; } + + /** + * Encrypts an input stream up to a given length. + * Exits early if the encrypted data is longer than the abandon length. + * + * @param sourceStream + * A InputStream object that represents the stream to measure. + * @param targetStream + * A ByteArrayOutputStream object that represents the stream to write the encrypted data. + * @param cipher + * The Cipher to use to encrypt the data. + * @param writeLength + * The number of bytes to read and encrypt from the sourceStream. + * @param abandonLength + * The number of bytes to read before the analysis is abandoned. Set this value to -1 to + * force the entire stream to be read. This parameter is provided to support upload thresholds. + * @return + * The size of the encrypted stream, or -1 if the encrypted stream would be over the abandonLength. + * @throws IOException + * If an I/O error occurs. + */ + public static long encryptStreamIfUnderThreshold(final InputStream sourceStream, final ByteArrayOutputStream targetStream, Cipher cipher, long writeLength, + long abandonLength) throws IOException { + if (abandonLength < 0) { + abandonLength = Long.MAX_VALUE; + } + + if (!sourceStream.markSupported()) { + throw new IllegalArgumentException(SR.INPUT_STREAM_SHOULD_BE_MARKABLE); + } + + sourceStream.mark(Constants.MAX_MARK_LENGTH); + + if (writeLength < 0) { + writeLength = Long.MAX_VALUE; + } + + CipherOutputStream encryptStream = new CipherOutputStream(targetStream, cipher); + + int count = -1; + long totalEncryptedLength = targetStream.size(); + final byte[] retrievedBuff = new byte[Constants.BUFFER_COPY_LENGTH]; + + int nextCopy = (int) Math.min(retrievedBuff.length, writeLength - totalEncryptedLength); + count = sourceStream.read(retrievedBuff, 0, nextCopy); + + while (nextCopy > 0 && count != -1) { + + // Note: We are flushing the CryptoStream on every write here. This way, we don't end up encrypting more data than we intend here, if + // we go over the abandonLength. + encryptStream.write(retrievedBuff, 0, count); + encryptStream.flush(); + totalEncryptedLength = targetStream.size(); + + if (totalEncryptedLength > abandonLength) { + // Abandon operation + break; + } + + nextCopy = (int) Math.min(retrievedBuff.length, writeLength - totalEncryptedLength); + count = sourceStream.read(retrievedBuff, 0, nextCopy); + } + + sourceStream.reset(); + sourceStream.mark(Constants.MAX_MARK_LENGTH); + + encryptStream.close(); + totalEncryptedLength = targetStream.size(); + if (totalEncryptedLength > abandonLength) { + totalEncryptedLength = -1; + } + + return totalEncryptedLength; + } /** * Asserts a continuation token is of the specified type. From 60e2f3c6719f8b0e584b65d11b040f24227017cd Mon Sep 17 00:00:00 2001 From: asorrin-msft Date: Thu, 24 Mar 2016 12:10:30 -0700 Subject: [PATCH 2/2] Fixing several table APIs to ignore the encryption options if encryption does not make sense. --- ChangeLog.txt | 1 + .../storage/table/TableEncryptionTests.java | 71 +++++++++++++++++++ .../azure/storage/table/CloudTable.java | 8 ++- .../azure/storage/table/CloudTableClient.java | 10 +-- .../storage/table/TableRequestOptions.java | 10 +++ 5 files changed, 94 insertions(+), 6 deletions(-) diff --git a/ChangeLog.txt b/ChangeLog.txt index 46eeaca40a4d0..86c093ad8c11b 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,6 +1,7 @@ 2015.10.05 Version 4.0-alpha-1 * Added support for client side encryption for blobs, queues and tables. * Since the encryption preview, added functionality where uploading encrypted blobs can be done with just PutBlob, not PutBlock + PutBlockList, if the blob is small enough. + * Since the encryption preview, fixed bugs in the Table Service where APIs such as 'CreateTable' were trying to encrypt their payload. Encryption is only supported on entities. 2015.10.05 Version 4.0.0 * Removed deprecated table AtomPub support. diff --git a/microsoft-azure-storage-test/src/com/microsoft/azure/storage/table/TableEncryptionTests.java b/microsoft-azure-storage-test/src/com/microsoft/azure/storage/table/TableEncryptionTests.java index d1f3ea1ba08e8..91c4a830d9b79 100644 --- a/microsoft-azure-storage-test/src/com/microsoft/azure/storage/table/TableEncryptionTests.java +++ b/microsoft-azure-storage-test/src/com/microsoft/azure/storage/table/TableEncryptionTests.java @@ -35,6 +35,8 @@ import com.microsoft.azure.keyvault.extensions.SymmetricKey; import com.microsoft.azure.storage.Constants; import com.microsoft.azure.storage.DictionaryKeyResolver; +import com.microsoft.azure.storage.ResultContinuation; +import com.microsoft.azure.storage.ResultSegment; import com.microsoft.azure.storage.StorageException; import com.microsoft.azure.storage.TestHelper; import com.microsoft.azure.storage.core.SR; @@ -1019,6 +1021,73 @@ public void testTableOperationEncryptionWithStrictModeOnMerge() throws InvalidKe } } + @Test + public void testTableOperationsIgnoreEncryption() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, URISyntaxException, StorageException + { + SymmetricKey aesKey = TestHelper.getSymmetricKey(); + TableRequestOptions options = new TableRequestOptions(); + options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); + options.setRequireEncryption(true); + + CloudTableClient tableClient = TableTestHelper.createCloudTableClient(); + CloudTable testTable = TableTestHelper.getRandomTableReference(); + + try + { + // Check Create() + testTable.create(options, null); + assertTrue("Table failed to be created when encryption policy was supplied.", testTable.exists()); + + // Check Exists() + assertTrue("Table.Exists() failed when encryption policy was supplied.", testTable.exists(options, null)); + + // Check ListTables() + for (String tableName : tableClient.listTables(testTable.getName(), options, null)) + { + assertEquals("ListTables failed when an encryption policy was specified.", testTable.getName(), tableName); + } + + // Check ListTablesSegmented() + for (String tableName : this.listAllTables(tableClient, testTable.getName(), options)) + { + assertEquals("ListTables failed when an encryption policy was specified.", testTable.getName(), tableName); + } + + // Check Get and Set Permissions + TablePermissions permissions = testTable.downloadPermissions(); + String policyName = "samplePolicy"; + SharedAccessTablePolicy tempPolicy = new SharedAccessTablePolicy(); + tempPolicy.setPermissionsFromString("r"); + tempPolicy.setSharedAccessExpiryTime(new Date()); + permissions.getSharedAccessPolicies().put(policyName, tempPolicy); + testTable.uploadPermissions(permissions, options, null); + assertTrue(testTable.downloadPermissions().getSharedAccessPolicies().containsKey(policyName)); + assertTrue(testTable.downloadPermissions(options, null).getSharedAccessPolicies().containsKey(policyName)); + + // Check Delete + testTable.delete(options, null); + assertFalse(testTable.exists()); + } + finally + { + testTable.deleteIfExists(); + } + } + + private ArrayList listAllTables(CloudTableClient tableClient, String prefix, TableRequestOptions options) throws StorageException + { + ResultContinuation token = null; + ArrayList tables = new ArrayList(); + + do + { + ResultSegment tableSegment = tableClient.listTablesSegmented(prefix, null, token, options, null); + tables.addAll(tableSegment.getResults()); + token = tableSegment.getContinuationToken(); + } while (token != null); + return tables; + } + private static DynamicTableEntity generateRandomEntity(String pk) { DynamicTableEntity ent = new DynamicTableEntity(); ent.getProperties().put("foo", new EntityProperty("bar")); @@ -1028,3 +1097,5 @@ private static DynamicTableEntity generateRandomEntity(String pk) { return ent; } } + + diff --git a/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTable.java b/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTable.java index 3611960e713b1..f4084dffdb2d5 100644 --- a/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTable.java +++ b/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTable.java @@ -241,6 +241,7 @@ public void create(TableRequestOptions options, OperationContext opContext) thro opContext.initialize(); options = TableRequestOptions.populateAndApplyDefaults(options, this.tableServiceClient); + options.clearEncryption(); Utility.assertNotNullOrEmpty("tableName", this.name); @@ -586,8 +587,9 @@ public Iterable execute(final TableQuery query, final EntityResolver) this.getServiceClient().generateIteratorForQuery(query, resolver, options, opContext); + return (Iterable) this.getServiceClient().generateIteratorForQuery(query, resolver, modifiedOptions, opContext); } /** @@ -643,8 +645,9 @@ public Iterable execute(final TableQuery query, fi final OperationContext opContext) { Utility.assertNotNull("query", query); Utility.assertNotNull(SR.QUERY_REQUIRES_VALID_CLASSTYPE_OR_RESOLVER, query.getClazzType()); + TableRequestOptions modifiedOptions = TableRequestOptions.populateAndApplyDefaults(options, this.getServiceClient()); query.setSourceTableName(this.getName()); - return (Iterable) this.getServiceClient().generateIteratorForQuery(query, null, options, opContext); + return (Iterable) this.getServiceClient().generateIteratorForQuery(query, null, modifiedOptions, opContext); } /** @@ -874,6 +877,7 @@ private boolean exists(final boolean primaryOnly, TableRequestOptions options, O opContext.initialize(); options = TableRequestOptions.populateAndApplyDefaults(options, this.tableServiceClient); + options.clearEncryption(); Utility.assertNotNullOrEmpty("tableName", this.name); diff --git a/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTableClient.java b/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTableClient.java index e92525ed5ff2f..d9fe7e206508b 100644 --- a/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTableClient.java +++ b/microsoft-azure-storage/src/com/microsoft/azure/storage/table/CloudTableClient.java @@ -198,8 +198,10 @@ public Iterable listTables(final String prefix) { @DoesServiceRequest public Iterable listTables(final String prefix, final TableRequestOptions options, final OperationContext opContext) { + TableRequestOptions modifiedOptions = TableRequestOptions.populateAndApplyDefaults(options, this); + modifiedOptions.clearEncryption(); return (Iterable) this.generateIteratorForQuery(this.generateListTablesQuery(prefix), - this.tableNameResolver, options, opContext); + this.tableNameResolver, modifiedOptions, opContext); } /** @@ -292,10 +294,11 @@ public ResultSegment listTablesSegmented(final String prefix, final Inte if (null != maxResults) { Utility.assertGreaterThanOrEqual("maxResults", maxResults, 1); } - + TableRequestOptions modifiedOptions = TableRequestOptions.populateAndApplyDefaults(options, this); + modifiedOptions.clearEncryption(); return (ResultSegment) this.executeQuerySegmentedImpl( this.generateListTablesQuery(prefix).take(maxResults), this.tableNameResolver, continuationToken, - options, opContext); + modifiedOptions, opContext); } /** @@ -587,7 +590,6 @@ protected Iterable generateIteratorForQuery(final } opContext.initialize(); - options = TableRequestOptions.populateAndApplyDefaults(options, this); SegmentedStorageRequest segmentedRequest = new SegmentedStorageRequest(); diff --git a/microsoft-azure-storage/src/com/microsoft/azure/storage/table/TableRequestOptions.java b/microsoft-azure-storage/src/com/microsoft/azure/storage/table/TableRequestOptions.java index cf21cde00f2ba..cd7e936ccfc54 100644 --- a/microsoft-azure-storage/src/com/microsoft/azure/storage/table/TableRequestOptions.java +++ b/microsoft-azure-storage/src/com/microsoft/azure/storage/table/TableRequestOptions.java @@ -149,6 +149,16 @@ protected static final TableRequestOptions populateAndApplyDefaults(final TableR TableRequestOptions.applyDefaults(modifiedOptions); return modifiedOptions; } + + /** + * Clears the encryption properties on this TableRequestOptions object. Useful for operations + * for which encryption does not make sense, such as CreateTable. + */ + protected void clearEncryption() { + this.setRequireEncryption(false); + this.setEncryptionPolicy(null); + this.setEncryptionResolver(null); + } /** * Applies defaults to the options passed in.