/** * Copyright Microsoft Corporation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.microsoft.azure.storage.table; import static org.junit.Assert.*; import java.net.HttpURLConnection; import java.net.URISyntaxException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Date; import java.util.NoSuchElementException; import java.util.UUID; import javax.crypto.BadPaddingException; import javax.crypto.NoSuchPaddingException; import org.junit.After; import org.junit.Before; import org.junit.Test; 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; import com.microsoft.azure.storage.table.TableRequestOptions.EncryptionResolver; import com.microsoft.azure.storage.table.TableRequestOptions.PropertyResolver; import com.microsoft.azure.storage.table.TableTestHelper.Class1; import com.microsoft.azure.storage.table.TableTestHelper.EncryptedClass1; import com.microsoft.azure.storage.table.TableTestHelper.ComplexEntity; import com.sun.jersey.core.util.Base64; public class TableEncryptionTests { CloudTable table = null; @Before public void tableTestMethodSetUp() throws URISyntaxException, StorageException { this.table = TableTestHelper.getRandomTableReference(); this.table.createIfNotExists(); } @After public void tableTestMethodTearDown() throws StorageException { this.table.deleteIfExists(); } @Test public void testTableOperationInsertDTEEncryption() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doInsertDynamicTableEntityEncryptionSync(TablePayloadFormat.Json); doInsertDynamicTableEntityEncryptionSync(TablePayloadFormat.JsonNoMetadata); doInsertDynamicTableEntityEncryptionSync(TablePayloadFormat.JsonFullMetadata); } private void doInsertDynamicTableEntityEncryptionSync(TablePayloadFormat format) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // insert Entity DynamicTableEntity ent = new DynamicTableEntity(UUID.randomUUID().toString(), new Date().toString()); ent.getProperties().put("foo2", new EntityProperty(Constants.EMPTY_STRING)); ent.getProperties().put("foo", new EntityProperty("bar")); ent.getProperties().put("fooint", new EntityProperty(1234)); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key == "foo" || key == "foo2") { return true; } return false; } }); this.table.execute(TableOperation.insert(ent), options, null); // retrieve Entity TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); retrieveOptions.setPropertyResolver(new PropertyResolver() { public EdmType propertyResolver(String pk, String rk, String key, String value) { if (key.equals("fooint")) { return EdmType.INT32; } return EdmType.STRING; } }); TableOperation operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), DynamicTableEntity.class); TableResult result = this.table.execute(operation, retrieveOptions, null); DynamicTableEntity retrievedEntity = (DynamicTableEntity) result.getResult(); assertNotNull(retrievedEntity); assertEquals(ent.getPartitionKey(), retrievedEntity.getPartitionKey()); assertEquals(ent.getRowKey(), retrievedEntity.getRowKey()); assertEquals(ent.getProperties().size(), retrievedEntity.getProperties().size()); assertEquals(ent.getProperties().get("foo").getValueAsString(), retrievedEntity.getProperties().get("foo").getValueAsString()); assertEquals(ent.getProperties().get("foo2").getValueAsString(), retrievedEntity.getProperties().get("foo2").getValueAsString()); assertEquals(ent.getProperties().get("fooint").getValueAsInteger(), retrievedEntity.getProperties().get("fooint").getValueAsInteger()); } @Test public void testTableOperationInsertPOCOEncryptionWithResolver() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doInsertPOCOEntityEncryptionWithResolver(TablePayloadFormat.Json); doInsertPOCOEntityEncryptionWithResolver(TablePayloadFormat.JsonNoMetadata); doInsertPOCOEntityEncryptionWithResolver(TablePayloadFormat.JsonFullMetadata); } private void doInsertPOCOEntityEncryptionWithResolver(TablePayloadFormat format) throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // insert Entity Class1 ent = new Class1(UUID.randomUUID().toString(), new Date().toString()); ent.populate(); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions insertOptions = new TableRequestOptions(); insertOptions.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); insertOptions.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.equals("A") || key.equals("foo")) { return true; } return false; } }); this.table.execute(TableOperation.insert(ent), insertOptions, null); // retrieve Entity // No need for an encryption resolver while retrieving the entity. TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); TableOperation operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), Class1.class); TableResult result = this.table.execute(operation, retrieveOptions, null); Class1 retrievedEntity = (Class1) result.getResult(); assertNotNull(retrievedEntity); assertEquals(ent.getPartitionKey(), retrievedEntity.getPartitionKey()); assertEquals(ent.getRowKey(), retrievedEntity.getRowKey()); retrievedEntity.validate(); } @Test public void testTableOperationInsertPOCOEncryptionWithAttributes() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doInsertPOCOEntityEncryptionWithAttributes(TablePayloadFormat.Json); doInsertPOCOEntityEncryptionWithAttributes(TablePayloadFormat.JsonNoMetadata); doInsertPOCOEntityEncryptionWithAttributes(TablePayloadFormat.JsonFullMetadata); } private void doInsertPOCOEntityEncryptionWithAttributes(TablePayloadFormat format) throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // insert Entity EncryptedClass1 ent = new EncryptedClass1(UUID.randomUUID().toString(), new Date().toString() + format); ent.populate(); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions insertOptions = new TableRequestOptions(); insertOptions.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); this.table.execute(TableOperation.insert(ent), insertOptions, null); // retrieve Entity // No need for an encryption resolver while retrieving the entity. TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); TableOperation operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), EncryptedClass1.class); TableResult result = this.table.execute(operation, retrieveOptions, null); EncryptedClass1 retrievedEntity = (EncryptedClass1) result.getResult(); assertNotNull(retrievedEntity); assertEquals(ent.getPartitionKey(), retrievedEntity.getPartitionKey()); assertEquals(ent.getRowKey(), retrievedEntity.getRowKey()); retrievedEntity.validate(); } @Test public void testTableOperationInsertPOCOEncryptionFailsWithClass() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doInsertPOCOEncryptionFailsWithClass(TablePayloadFormat.Json); doInsertPOCOEncryptionFailsWithClass(TablePayloadFormat.JsonNoMetadata); doInsertPOCOEncryptionFailsWithClass(TablePayloadFormat.JsonFullMetadata); } private void doInsertPOCOEncryptionFailsWithClass(TablePayloadFormat format) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // insert Entity EncryptedClass1 ent = new EncryptedClass1(UUID.randomUUID().toString(), new Date().toString()); ent.populate(); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions insertOptions = new TableRequestOptions(); insertOptions.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); insertOptions.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.equals("B")) { return true; } return false; } }); // Since we have specified attributes and resolver, properties A, B and foo will be encrypted. this.table.execute(TableOperation.insert(ent), insertOptions, null); // retrieve entity without decryption and confirm that all 3 properties were encrypted. // No need for an encryption resolver while retrieving the entity. TableOperation operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), EncryptedClass1.class); TableResult result = this.table.execute(operation, null, null); EncryptedClass1 retrievedEntity = result.getResultAsType(); assertNotNull(retrievedEntity); assertNotNull(retrievedEntity.getC()); // Assert that we ignore encrypted entities due to bad type, except in no metadata which // assumes string if (format == TablePayloadFormat.JsonNoMetadata) { assertNotNull(retrievedEntity.getB()); } else { assertNull(retrievedEntity.getB()); } } @Test public void testTableOperationInsertPOCOEncryptionWithAttributesAndResolver() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doInsertPOCOEntityEncryptionWithAttributesAndResolver(TablePayloadFormat.Json); doInsertPOCOEntityEncryptionWithAttributesAndResolver(TablePayloadFormat.JsonNoMetadata); doInsertPOCOEntityEncryptionWithAttributesAndResolver(TablePayloadFormat.JsonFullMetadata); } private void doInsertPOCOEntityEncryptionWithAttributesAndResolver(TablePayloadFormat format) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // insert Entity EncryptedClass1 ent = new EncryptedClass1(UUID.randomUUID().toString(), new Date().toString()); ent.populate(); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions insertOptions = new TableRequestOptions(); insertOptions.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); insertOptions.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.equals("B")) { return true; } return false; } }); // Since we have specified attributes and resolver, properties A, B and foo will be encrypted. this.table.execute(TableOperation.insert(ent), insertOptions, null); // retrieve entity without decryption and confirm that all 3 properties were encrypted. // No need for an encryption resolver while retrieving the entity. TableOperation operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), DynamicTableEntity.class); TableResult result = this.table.execute(operation, null, null); DynamicTableEntity retrievedDynamicEntity = (DynamicTableEntity) result.getResult(); assertNotNull(retrievedDynamicEntity); assertEquals(ent.getPartitionKey(), retrievedDynamicEntity.getPartitionKey()); assertEquals(ent.getRowKey(), retrievedDynamicEntity.getRowKey()); if (format == TablePayloadFormat.JsonNoMetadata) { assertEquals(EdmType.STRING, retrievedDynamicEntity.getProperties().get("A").getEdmType()); assertEquals(EdmType.STRING, retrievedDynamicEntity.getProperties().get("B").getEdmType()); } else { assertEquals(EdmType.BINARY, retrievedDynamicEntity.getProperties().get("A").getEdmType()); assertEquals(EdmType.BINARY, retrievedDynamicEntity.getProperties().get("B").getEdmType()); } // retrieve entity and decrypt. TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), EncryptedClass1.class); result = this.table.execute(operation, retrieveOptions, null); EncryptedClass1 retrievedEntity = (EncryptedClass1) result.getResult(); assertNotNull(retrievedEntity); assertEquals(ent.getPartitionKey(), retrievedEntity.getPartitionKey()); assertEquals(ent.getRowKey(), retrievedEntity.getRowKey()); retrievedEntity.validate(); } @Test public void testTableQueryPOCOProjectionEncryption() throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { // insert Entity EncryptedClass1 ent1 = new EncryptedClass1(UUID.randomUUID().toString(), new Date().toString()); ent1.populate(); EncryptedClass1 ent2 = new EncryptedClass1(UUID.randomUUID().toString(), new Date().toString()); ent2.populate(); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); this.table.execute(TableOperation.insert(ent1), options, null); this.table.execute(TableOperation.insert(ent2), options, null); // Query with different payload formats. doTableQueryPOCOProjectionEncryption(TablePayloadFormat.Json, aesKey); doTableQueryPOCOProjectionEncryption(TablePayloadFormat.JsonNoMetadata, aesKey); doTableQueryPOCOProjectionEncryption(TablePayloadFormat.JsonFullMetadata, aesKey); } private void doTableQueryPOCOProjectionEncryption(TablePayloadFormat format, SymmetricKey aesKey) { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); TableQuery<EncryptedClass1> query = TableQuery.from(EncryptedClass1.class).select(new String[] { "A", "C" }); for (EncryptedClass1 ent : this.table.execute(query, options, null)) { assertNotNull(ent.getPartitionKey()); assertNotNull(ent.getRowKey()); assertNotNull(ent.getTimestamp()); assertEquals(ent.getA(), "foo_A"); assertNull(ent.getB()); assertEquals(ent.getC(), "foo_C"); assertNull(ent.getD()); } } @Test public void testTableQueryDTEProjectionEncryption() throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { // insert Entity DynamicTableEntity ent1 = new DynamicTableEntity(UUID.randomUUID().toString(), new Date().toString()); ent1.getProperties().put("A", new EntityProperty(Constants.EMPTY_STRING)); ent1.getProperties().put("B", new EntityProperty("b")); DynamicTableEntity ent2 = new DynamicTableEntity(UUID.randomUUID().toString(), new Date().toString()); ent2.getProperties().put("A", new EntityProperty("a")); ent2.getProperties().put("B", new EntityProperty("b")); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.equals("A")) { return true; } return false; } }); this.table.execute(TableOperation.insert(ent1), options, null); this.table.execute(TableOperation.insert(ent2), options, null); // Query with different payload formats. doTableQueryDTEProjectionEncryption(TablePayloadFormat.Json, aesKey); doTableQueryDTEProjectionEncryption(TablePayloadFormat.JsonNoMetadata, aesKey); doTableQueryDTEProjectionEncryption(TablePayloadFormat.JsonFullMetadata, aesKey); } private void doTableQueryDTEProjectionEncryption(TablePayloadFormat format, SymmetricKey aesKey) { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); TableQuery<DynamicTableEntity> query = TableQuery.from(DynamicTableEntity.class).select(new String[] { "A" }); for (DynamicTableEntity ent : this.table.execute(query, options, null)) { assertNotNull(ent.getPartitionKey()); assertNotNull(ent.getRowKey()); assertNotNull(ent.getTimestamp()); assertTrue(ent.getProperties().get("A").getValueAsString().equals("a") || ent.getProperties().get("A").getValueAsString().equals(Constants.EMPTY_STRING)); } // Test to make sure that we don't specify encryption columns when there aren't any columns specified at all. query = TableQuery.from(DynamicTableEntity.class); for (DynamicTableEntity ent : this.table.execute(query, options, null)) { assertNotNull(ent.getPartitionKey()); assertNotNull(ent.getRowKey()); assertNotNull(ent.getTimestamp()); assertTrue(ent.getProperties().get("A").getValueAsString().equals("a") || ent.getProperties().get("A").getValueAsString().equals(Constants.EMPTY_STRING)); } } @Test public void testTableOperationReplaceEncryption() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doTableOperationReplaceEncryption(TablePayloadFormat.Json); doTableOperationReplaceEncryption(TablePayloadFormat.JsonNoMetadata); doTableOperationReplaceEncryption(TablePayloadFormat.JsonFullMetadata); } private void doTableOperationReplaceEncryption(TablePayloadFormat format) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.equals("A") || key.equals("B")) { return true; } return false; } }); // insert Entity DynamicTableEntity baseEntity = new DynamicTableEntity("test", "foo" + format.toString()); baseEntity.getProperties().put("A", new EntityProperty("a")); this.table.execute(TableOperation.insert(baseEntity), options, null); // ReplaceEntity DynamicTableEntity replaceEntity = new DynamicTableEntity(baseEntity.getPartitionKey(), baseEntity.getRowKey()); replaceEntity.setEtag(baseEntity.getEtag()); replaceEntity.getProperties().put("B", new EntityProperty("b")); this.table.execute(TableOperation.replace(replaceEntity), options, null); // retrieve Entity & Verify Contents // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); TableResult result = this.table .execute(TableOperation.retrieve(baseEntity.getPartitionKey(), baseEntity.getRowKey(), DynamicTableEntity.class), retrieveOptions, null); DynamicTableEntity retrievedEntity = (DynamicTableEntity) result.getResult(); assertNotNull(retrievedEntity); assertEquals(replaceEntity.getProperties().size(), retrievedEntity.getProperties().size()); assertEquals(replaceEntity.getProperties().get("B").getValueAsString(), retrievedEntity.getProperties().get("B").getValueAsString()); } @Test public void testTableBatchinsertOrReplaceEncryption() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doTableBatchInsertOrReplaceEncryption(TablePayloadFormat.Json); doTableBatchInsertOrReplaceEncryption(TablePayloadFormat.JsonNoMetadata); doTableBatchInsertOrReplaceEncryption(TablePayloadFormat.JsonFullMetadata); } private void doTableBatchInsertOrReplaceEncryption(TablePayloadFormat format) throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.equals("A") || key.equals("B")) { return true; } return false; } }); // insert Or Replace with no pre-existing entity DynamicTableEntity insertOrReplaceEntity = new DynamicTableEntity("insertOrReplace entity", "foo" + format.toString()); insertOrReplaceEntity.getProperties().put("A", new EntityProperty("a")); TableBatchOperation batch = new TableBatchOperation(); batch.insertOrReplace(insertOrReplaceEntity); this.table.execute(batch, options, null); // retrieve Entity & Verify Contents // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); TableResult result = this.table.execute(TableOperation.retrieve(insertOrReplaceEntity.getPartitionKey(), insertOrReplaceEntity.getRowKey(), DynamicTableEntity.class), retrieveOptions, null); DynamicTableEntity retrievedEntity = (DynamicTableEntity) result.getResult(); assertNotNull(retrievedEntity); assertEquals(insertOrReplaceEntity.getProperties().size(), retrievedEntity.getProperties().size()); DynamicTableEntity replaceEntity = new DynamicTableEntity(insertOrReplaceEntity.getPartitionKey(), insertOrReplaceEntity.getRowKey()); replaceEntity.getProperties().put("B", new EntityProperty("b")); TableBatchOperation batch2 = new TableBatchOperation(); batch2.insertOrReplace(replaceEntity); this.table.execute(batch2, options, null); // retrieve Entity & Verify Contents result = this.table.execute(TableOperation.retrieve(insertOrReplaceEntity.getPartitionKey(), insertOrReplaceEntity.getRowKey(), DynamicTableEntity.class), retrieveOptions, null); retrievedEntity = (DynamicTableEntity) result.getResult(); assertNotNull(retrievedEntity); assertEquals(1, retrievedEntity.getProperties().size()); assertEquals(replaceEntity.getProperties().get("B").getValueAsString(), retrievedEntity.getProperties().get("B").getValueAsString()); } @Test public void testTableBatchRetrieveEncryptedEntity() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doTableBatchRetrieveEncryptedEntitySync(TablePayloadFormat.Json); doTableBatchRetrieveEncryptedEntitySync(TablePayloadFormat.JsonNoMetadata); doTableBatchRetrieveEncryptedEntitySync(TablePayloadFormat.JsonFullMetadata); } private void doTableBatchRetrieveEncryptedEntitySync(TablePayloadFormat format) throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.equals("A") || key.equals("B")) { return true; } return false; } }); // add insert DynamicTableEntity sendEnt = generateRandomEntity(UUID.randomUUID().toString()); // generate a set of properties for all supported Types sendEnt.setProperties(new ComplexEntity().writeEntity(null)); sendEnt.getProperties().put("foo", new EntityProperty("bar")); TableBatchOperation batch = new TableBatchOperation(); TableOperation retrieve = TableOperation.retrieve(sendEnt.getPartitionKey(), sendEnt.getRowKey(), DynamicTableEntity.class); batch.add(retrieve); // not found ArrayList<TableResult> results = this.table.execute(batch, options, null); assertEquals(results.size(), 1); assertEquals(results.get(0).getHttpStatusCode(), HttpURLConnection.HTTP_NOT_FOUND); assertNull(results.get(0).getResult()); assertNull(results.get(0).getEtag()); // insert entity this.table.execute(TableOperation.insert(sendEnt), options, null); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); // Success results = this.table.execute(batch, retrieveOptions, null); assertEquals(results.size(), 1); assertEquals(results.get(0).getHttpStatusCode(), HttpURLConnection.HTTP_OK); DynamicTableEntity retrievedEntity = (DynamicTableEntity) results.get(0).getResult(); // Validate entity assertEquals(sendEnt.getProperties().get("foo").getValueAsString(), retrievedEntity.getProperties().get("foo").getValueAsString()); } @Test public void testTableOperationValidateEncryption() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { doTableOperationValidateEncryption(TablePayloadFormat.Json); doTableOperationValidateEncryption(TablePayloadFormat.JsonNoMetadata); doTableOperationValidateEncryption(TablePayloadFormat.JsonFullMetadata); } private void doTableOperationValidateEncryption(TablePayloadFormat format) throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { this.table.getServiceClient().getDefaultRequestOptions().setTablePayloadFormat(format); // insert Entity DynamicTableEntity ent = new DynamicTableEntity(UUID.randomUUID().toString(), new Date().toString()); ent.getProperties().put("encprop", new EntityProperty(Constants.EMPTY_STRING)); ent.getProperties().put("encprop2", new EntityProperty(Constants.EMPTY_STRING)); ent.getProperties().put("encprop3", new EntityProperty("bar")); ent.getProperties().put("notencprop", new EntityProperty(1234)); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions uploadOptions = new TableRequestOptions(); uploadOptions.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); uploadOptions.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.startsWith("encprop")) { return true; } return false; } }); uploadOptions.setPropertyResolver(new PropertyResolver() { public EdmType propertyResolver(String pk, String rk, String key, String value) { if (key == "notencprop") { return EdmType.INT32; } return EdmType.STRING; } }); this.table.execute(TableOperation.insert(ent), uploadOptions, null); TableRequestOptions downloadOptions = new TableRequestOptions(); downloadOptions.setPropertyResolver(new PropertyResolver() { public EdmType propertyResolver(String pk, String rk, String key, String value) { if (key == "notencprop") { return EdmType.INT32; } return EdmType.STRING; } }); // retrieve Entity without decrypting TableOperation operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), DynamicTableEntity.class); TableResult result = this.table.execute(operation, downloadOptions, null); DynamicTableEntity retrievedEntity = (DynamicTableEntity) result.getResult(); assertNotNull(retrievedEntity); assertEquals(ent.getPartitionKey(), retrievedEntity.getPartitionKey()); assertEquals(ent.getRowKey(), retrievedEntity.getRowKey()); // Properties having the same value should be encrypted to different values. if (format == TablePayloadFormat.JsonNoMetadata) { // With DTE and Json no metadata, if an encryption policy is not set, the client lib just reads the byte arrays as strings. assertFalse(ent.getProperties().get("encprop").getValueAsString() .equals(retrievedEntity.getProperties().get("encprop").getValueAsString())); } else { assertFalse(retrievedEntity.getProperties().get("encprop").getValueAsByteArray() .equals(retrievedEntity.getProperties().get("encprop2").getValueAsByteArray())); assertFalse(ent.getProperties().get("encprop").getEdmType() .equals(retrievedEntity.getProperties().get("encprop").getEdmType())); assertFalse(ent.getProperties().get("encprop2").getEdmType() .equals(retrievedEntity.getProperties().get("encprop2").getEdmType())); assertFalse(ent.getProperties().get("encprop3").getEdmType() .equals(retrievedEntity.getProperties().get("encprop3").getEdmType())); } assertEquals(ent.getProperties().get("notencprop").getValueAsInteger(), retrievedEntity.getProperties().get("notencprop").getValueAsInteger()); } @Test public void testTableEncryptingUnsupportedPropertiesShouldThrow() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { // insert Entity DynamicTableEntity ent = new DynamicTableEntity(UUID.randomUUID().toString(), new Date().toString()); ent.getProperties().put("foo2", new EntityProperty(Constants.EMPTY_STRING)); ent.getProperties().put("fooint", new EntityProperty(1234)); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key.startsWith("foo")) { return true; } return false; } }); try { this.table.execute(TableOperation.insert(ent), options, null); fail("Encrypting non-String properties should fail"); } catch (StorageException e) { assertEquals(IllegalArgumentException.class, e.getCause().getClass()); } ent.getProperties().remove("fooint"); ent.getProperties().put("foo", null); try { this.table.execute(TableOperation.insert(ent), options, null); fail("Encrypting null properties should fail"); } catch (StorageException e) { assertEquals(IllegalArgumentException.class, e.getCause().getClass()); } } @Test public void testTableEncryptionValidateSwappingPropertiesThrows() throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key == "Prop1") { return true; } return false; } }); // Insert Entities DynamicTableEntity baseEntity1 = new DynamicTableEntity("test1", "foo1"); baseEntity1.getProperties().put("Prop1", new EntityProperty("Value1")); this.table.execute(TableOperation.insert(baseEntity1), options, null); DynamicTableEntity baseEntity2 = new DynamicTableEntity("test1", "foo2"); baseEntity2.getProperties().put("Prop1", new EntityProperty("Value2")); this.table.execute(TableOperation.insert(baseEntity2), options, null); // Retrieve entity1 (Do not set encryption policy) TableResult result = this.table.execute(TableOperation.retrieve(baseEntity1.getPartitionKey(), baseEntity1.getRowKey(), DynamicTableEntity.class)); DynamicTableEntity retrievedEntity = (DynamicTableEntity) result.getResult(); // Replace entity2 with encrypted entity1's properties (Do not set encryption policy). DynamicTableEntity replaceEntity = new DynamicTableEntity(baseEntity2.getPartitionKey(), baseEntity2.getRowKey(), baseEntity2.getEtag(), retrievedEntity.getProperties()); this.table.execute(TableOperation.replace(replaceEntity)); // Try to retrieve entity2 // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); try { result = this.table.execute(TableOperation.retrieve(baseEntity2.getPartitionKey(), baseEntity2.getRowKey(), DynamicTableEntity.class), retrieveOptions, null); fail(); } catch (StorageException ex) { assertEquals(BadPaddingException.class, ex.getCause().getClass()); } } @Test public void testTableOperationEncryptionWithStrictMode() throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { // Insert Entity DynamicTableEntity ent = new DynamicTableEntity(UUID.randomUUID().toString(), new Date().toString()); ent.getProperties().put("foo2", new EntityProperty(Constants.EMPTY_STRING)); ent.getProperties().put("foo", new EntityProperty("bar")); ent.getProperties().put("fooint", new EntityProperty(1234)); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { if (key == "foo" || key == "foo2") { return true; } return false; } }); options.setRequireEncryption(true); this.table.execute(TableOperation.insert(ent), options, null); // Insert an entity when RequireEncryption is set to true but no policy is specified. This should throw. options.setEncryptionPolicy(null); try { this.table.execute(TableOperation.insert(ent), options, null); fail("Not specifying a policy when RequireEncryption is set to true should throw."); } catch (IllegalArgumentException ex) { } // Retrieve Entity TableRequestOptions retrieveOptions = new TableRequestOptions(); retrieveOptions.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); retrieveOptions.setPropertyResolver(new PropertyResolver() { public EdmType propertyResolver(String pk, String rk, String key, String value) { if (key == "fooint") { return EdmType.INT32; } return EdmType.STRING; } }); retrieveOptions.setRequireEncryption(true); TableOperation operation = TableOperation.retrieve(ent.getPartitionKey(), ent.getRowKey(), DynamicTableEntity.class); TableResult result = this.table.execute(operation, retrieveOptions, null); DynamicTableEntity retrievedEntity = (DynamicTableEntity) result.getResult(); // Replace entity with plain text. ent.setEtag(retrievedEntity.getEtag()); this.table.execute(TableOperation.replace(ent)); // Retrieve with RequireEncryption flag but no metadata on the service. This should throw. try { this.table.execute(operation, retrieveOptions, null); fail("Retrieving with RequireEncryption set to true and no metadata on the service should fail."); } catch (StorageException ex) { } // Set RequireEncryption flag to true and retrieve. retrieveOptions.setRequireEncryption(false); result = this.table.execute(operation, retrieveOptions, null); } @Test public void testTableQueryEncryptionMixedMode() throws StorageException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException { // insert Entity EncryptedClass1 ent1 = new EncryptedClass1(UUID.randomUUID().toString(), new Date().toString()); ent1.populate(); EncryptedClass1 ent2 = new EncryptedClass1(UUID.randomUUID().toString(), new Date().toString()); ent2.populate(); // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); // Insert an encrypted entity. this.table.execute(TableOperation.insert(ent1), options, null); // Insert a non-encrypted entity. this.table.execute(TableOperation.insert(ent2), null, null); // Create the resolver to be used for unwrapping. DictionaryKeyResolver resolver = new DictionaryKeyResolver(); resolver.add(aesKey); options = new TableRequestOptions(); options.setEncryptionPolicy(new TableEncryptionPolicy(null, resolver)); // Set RequireEncryption to false and query. This will succeed. options.setRequireEncryption(false); TableQuery<EncryptedClass1> query = TableQuery.from(EncryptedClass1.class); this.table.execute(query, options, null).iterator().next(); // Set RequireEncryption to true and query. This will fail because it can't find the metadata for the second enctity on the server. options.setRequireEncryption(true); try { this.table.execute(query, options, null).iterator().next(); fail("All entities retrieved should be encrypted when RequireEncryption is set to true."); } catch (NoSuchElementException ex) { assertEquals(StorageException.class, ex.getCause().getClass()); } } @Test public void testTableOperationEncryptionWithStrictModeOnMerge() throws InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, StorageException { // Insert Entity DynamicTableEntity ent = new DynamicTableEntity(UUID.randomUUID().toString(), new Date().toString()); ent.getProperties().put("foo2", new EntityProperty(Constants.EMPTY_STRING)); ent.getProperties().put("foo", new EntityProperty("bar")); ent.getProperties().put("fooint", new EntityProperty(1234)); ent.setEtag("*"); TableRequestOptions options = new TableRequestOptions(); options.setRequireEncryption(true); try { this.table.execute(TableOperation.merge(ent), options, null); fail("Merge with RequireEncryption on should fail."); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), SR.ENCRYPTION_POLICY_MISSING_IN_STRICT_MODE); } try { this.table.execute(TableOperation.insertOrMerge(ent), options, null); fail("InsertOrMerge with RequireEncryption on should fail."); } catch (IllegalArgumentException ex) { assertEquals(ex.getMessage(), SR.ENCRYPTION_POLICY_MISSING_IN_STRICT_MODE); } // Create the Key to be used for wrapping. SymmetricKey aesKey = TestHelper.getSymmetricKey(); options.setEncryptionPolicy(new TableEncryptionPolicy(aesKey, null)); try { this.table.execute(TableOperation.merge(ent), options, null); fail("Merge with an EncryptionPolicy should fail."); } catch (StorageException ex) { assertEquals(ex.getMessage(), SR.ENCRYPTION_NOT_SUPPORTED_FOR_OPERATION); } try { this.table.execute(TableOperation.insertOrMerge(ent), options, null); fail("InsertOrMerge with an EncryptionPolicy should fail."); } catch (StorageException ex) { assertEquals(ex.getMessage(), SR.ENCRYPTION_NOT_SUPPORTED_FOR_OPERATION); } } @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(); } } @Test public void testCrossPlatformCompatibility() throws StorageException, URISyntaxException { CloudTable testTable = TableTestHelper.getRandomTableReference(); try { testTable.createIfNotExists(); // Hard code some sample data, then see if we can decrypt it. // This key is used only for test, do not use to encrypt any sensitive data. SymmetricKey sampleKEK = new SymmetricKey("key1", Base64.decode("rFz7+tv4hRiWdWUJMFlxl1xxtU/qFUeTriGaxwEcxjU=")); // This data here was created using Fiddler to capture the .NET library uploading an encrypted entity, encrypted with the specified KEK and CEK. // Note that this data is lacking the library information in the KeyWrappingMetadata. DynamicTableEntity dteNetOld = new DynamicTableEntity("pk", "netUp"); dteNetOld.getProperties().put("sampleProp", new EntityProperty(Base64.decode("27cLSlSFqy9C0xUCr57XAA=="))); dteNetOld.getProperties().put("sampleProp2", new EntityProperty(Base64.decode("pZR6Ln/DwbwyyOCEezL/hg=="))); dteNetOld.getProperties().put("sampleProp3", new EntityProperty(Base64.decode("JOix4N8eX/WuCtIvlD2QxQ=="))); dteNetOld.getProperties().put("_ClientEncryptionMetadata1", new EntityProperty("{\"WrappedContentKey\":{\"KeyId\":\"key1\",\"EncryptedKey\":\"pwSKxpJkwCS2zCaykh0m8e4OApeLuQ4FiahZ9zdwxaLL1HsWqQ4DSw==\",\"Algorithm\":\"A256KW\"},\"EncryptionAgent\":{\"Protocol\":\"1.0\",\"EncryptionAlgorithm\":\"AES_CBC_256\"},\"ContentEncryptionIV\":\"obTAQcYeFQ3IU7Jfcema7Q==\",\"KeyWrappingMetadata\":{}}")); dteNetOld.getProperties().put("_ClientEncryptionMetadata2", new EntityProperty(Base64.decode("MWA7LlvXSJnKhf8f7MVhfjWECkxrCyCXGIlYY6ucpr34IVDU7fN6IHvKxV15WiXp"))); testTable.execute(TableOperation.insert(dteNetOld)); // This data here was created using Fiddler to capture the Java library uploading an encrypted entity, encrypted with the specified KEK and CEK. // Note that this data is lacking the KeyWrappingMetadata. It also constructs an IV with PK + RK + column name. DynamicTableEntity dteJavaOld = new DynamicTableEntity("pk", "javaUp"); dteJavaOld.getProperties().put("sampleProp", new EntityProperty(Base64.decode("sa3bCvXq79ImSPveChS+cg=="))); dteJavaOld.getProperties().put("sampleProp2", new EntityProperty(Base64.decode("KXjuBNn9DesCmMcdVpamJw=="))); dteJavaOld.getProperties().put("sampleProp3", new EntityProperty(Base64.decode("wykVEni1rV+H6oNjoNml6A=="))); dteJavaOld.getProperties().put("_ClientEncryptionMetadata1", new EntityProperty("{\"WrappedContentKey\":{\"KeyId\":\"key1\",\"EncryptedKey\":\"2F4rIuDmGPgEmhpvTtE7x6281BetKz80EsgRwGxTjL8rRt7Z7GrOgg==\",\"Algorithm\":\"A256KW\"},\"EncryptionAgent\":{\"Protocol\":\"1.0\",\"EncryptionAlgorithm\":\"AES_CBC_256\"},\"ContentEncryptionIV\":\"8st/uXffG+6DxBhw4D1URw==\"}")); dteJavaOld.getProperties().put("_ClientEncryptionMetadata2", new EntityProperty(Base64.decode("WznUoytxkvl9KhZ4mNlqkBvRTUHN/D5IgJmNl7kQBOtFBOSgZZrTfZXKH8GjmvKA"))); testTable.execute(TableOperation.insert(dteJavaOld)); TableEncryptionPolicy policy = new TableEncryptionPolicy(sampleKEK, null); TableRequestOptions options = new TableRequestOptions(); options.setEncryptionPolicy(policy); options.setEncryptionResolver(new EncryptionResolver() { public boolean encryptionResolver(String pk, String rk, String key) { return true; } }); for (DynamicTableEntity dte : testTable.execute(TableQuery.from(DynamicTableEntity.class), options, null)) { assertTrue("String not properly decoded.", dte.getProperties().get("sampleProp").getValueAsString().equals("sampleValue")); assertTrue("String not properly decoded.", dte.getProperties().get("sampleProp2").getValueAsString().equals("sampleValue")); assertTrue("String not properly decoded.", dte.getProperties().get("sampleProp3").getValueAsString().equals("sampleValue")); assertEquals("Incorrect number or properties", dte.getProperties().size(), 3); } } finally { if (testTable != null) { testTable.deleteIfExists(); } } } private ArrayList<String> listAllTables(CloudTableClient tableClient, String prefix, TableRequestOptions options) throws StorageException { ResultContinuation token = null; ArrayList<String> tables = new ArrayList<String>(); do { ResultSegment<String> 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")); ent.setPartitionKey(pk); ent.setRowKey(UUID.randomUUID().toString()); return ent; } }