diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index 7396437ecaafd..faeaca0d37044 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -188,7 +188,7 @@ com.azure azure-cosmos-encryption - 1.12.0-beta.1 + 2.0.0-beta.1 com.azure diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 863d6191adb80..7318b2243b8da 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -96,7 +96,7 @@ com.azure.cosmos.spark:azure-cosmos-spark_3_2-12;1.0.0-beta.1;1.0.0-beta.1 com.azure.cosmos.spark:azure-cosmos-spark_3-1_2-12;4.17.0;4.18.0-beta.1 com.azure.cosmos.spark:azure-cosmos-spark_3-2_2-12;4.17.0;4.18.0-beta.1 com.azure.cosmos.spark:azure-cosmos-spark_3-3_2-12;4.17.0;4.18.0-beta.1 -com.azure:azure-cosmos-encryption;1.11.0;1.12.0-beta.1 +com.azure:azure-cosmos-encryption;1.11.0;2.0.0-beta.1 com.azure:azure-data-appconfiguration;1.4.1;1.5.0-beta.1 com.azure:azure-data-appconfiguration-perf;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-data-schemaregistry;1.3.2;1.4.0-beta.3 @@ -416,4 +416,4 @@ com.azure.tools:azure-sdk-build-tool;1.0.0-beta.1;1.0.0-beta.2 # Format; # beta_:;dependency-version # note: Released beta versions will not be manipulated with the automatic PR creation code. -beta_com.azure:azure-communication-common;1.3.0-beta.1 +beta_com.azure:azure-communication-common;1.3.0-beta.1 \ No newline at end of file diff --git a/sdk/cosmos/azure-cosmos-benchmark/pom.xml b/sdk/cosmos/azure-cosmos-benchmark/pom.xml index e982c0a74ab7f..8a8aeadf86306 100644 --- a/sdk/cosmos/azure-cosmos-benchmark/pom.xml +++ b/sdk/cosmos/azure-cosmos-benchmark/pom.xml @@ -57,7 +57,7 @@ Licensed under the MIT License. com.azure azure-cosmos-encryption - 1.12.0-beta.1 + 2.0.0-beta.1 diff --git a/sdk/cosmos/azure-cosmos-benchmark/src/main/java/com/azure/cosmos/benchmark/encryption/AsyncEncryptionBenchmark.java b/sdk/cosmos/azure-cosmos-benchmark/src/main/java/com/azure/cosmos/benchmark/encryption/AsyncEncryptionBenchmark.java index fc4f2ff397ecf..0a5f7446501df 100644 --- a/sdk/cosmos/azure-cosmos-benchmark/src/main/java/com/azure/cosmos/benchmark/encryption/AsyncEncryptionBenchmark.java +++ b/sdk/cosmos/azure-cosmos-benchmark/src/main/java/com/azure/cosmos/benchmark/encryption/AsyncEncryptionBenchmark.java @@ -549,7 +549,7 @@ private void createEncryptionDatabaseAndContainer() { includedPath.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); encryptionPaths.add(includedPath); } - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(encryptionPaths); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(encryptionPaths, 2); CosmosContainerProperties containerProperties = new CosmosContainerProperties(this.configuration.getCollectionId(), Configuration.DEFAULT_PARTITION_KEY_PATH); diff --git a/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md b/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md index bbba4b4e95b64..ddb40000756d4 100644 --- a/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md +++ b/sdk/cosmos/azure-cosmos-encryption/CHANGELOG.md @@ -1,10 +1,13 @@ ## Release History -### 1.12.0-beta.1 (Unreleased) + +### 2.0.0-beta.1 (Unreleased) #### Features Added +* Added support for allowing partition key path and id to be part of client encryption policy - See [PR 30678](https://github.com/Azure/azure-sdk-for-java/pull/30678) #### Breaking Changes +* Adds code to support ParititonKey and Id encryption, when the PolicyFormatVersion is set to 2 - See [PR 30678](https://github.com/Azure/azure-sdk-for-java/pull/30678) #### Bugs Fixed diff --git a/sdk/cosmos/azure-cosmos-encryption/pom.xml b/sdk/cosmos/azure-cosmos-encryption/pom.xml index b43d7cfc7eb4f..7ceda2a0193fc 100644 --- a/sdk/cosmos/azure-cosmos-encryption/pom.xml +++ b/sdk/cosmos/azure-cosmos-encryption/pom.xml @@ -13,7 +13,7 @@ Licensed under the MIT License. com.azure azure-cosmos-encryption - 1.12.0-beta.1 + 2.0.0-beta.1 Encryption Plugin for Azure Cosmos DB SDK This Package contains Encryption Plugin for Microsoft Azure Cosmos SDK jar diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java index 9de68048575ff..aa42d6639164b 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncClient.java @@ -222,7 +222,7 @@ private CosmosContainerProperties getContainerPropertiesWithVersionValidation(Co throw new IllegalArgumentException("Container without client encryption policy cannot be used"); } - if (cosmosContainerResponse.getProperties().getClientEncryptionPolicy().getPolicyFormatVersion() > 1) { + if (cosmosContainerResponse.getProperties().getClientEncryptionPolicy().getPolicyFormatVersion() > 2) { throw new UnsupportedOperationException("This version of the Encryption library cannot be used with this " + "container. Please upgrade to the latest version of the same."); } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java index 36675fb7c08d5..aeb9672090a78 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/CosmosEncryptionAsyncContainer.java @@ -10,7 +10,11 @@ import com.azure.cosmos.encryption.implementation.Constants; import com.azure.cosmos.encryption.implementation.CosmosResponseFactory; import com.azure.cosmos.encryption.implementation.EncryptionImplementationBridgeHelpers; +import com.azure.cosmos.encryption.implementation.EncryptionProcessor; +import com.azure.cosmos.encryption.implementation.EncryptionSettings; import com.azure.cosmos.encryption.implementation.EncryptionUtils; +import com.azure.cosmos.encryption.implementation.mdesrc.cryptography.MicrosoftDataEncryptionException; +import com.azure.cosmos.encryption.models.SqlQuerySpecWithEncryption; import com.azure.cosmos.implementation.CosmosPagedFluxOptions; import com.azure.cosmos.implementation.HttpConstants; import com.azure.cosmos.implementation.ImplementationBridgeHelpers; @@ -39,13 +43,14 @@ import com.azure.cosmos.models.FeedResponse; import com.azure.cosmos.models.ModelBridgeInternal; import com.azure.cosmos.models.PartitionKey; +import com.azure.cosmos.models.PartitionKeyBuilder; import com.azure.cosmos.models.SqlParameter; import com.azure.cosmos.models.SqlQuerySpec; import com.azure.cosmos.util.CosmosPagedFlux; import com.azure.cosmos.util.UtilBridgeInternal; -import com.azure.cosmos.encryption.implementation.EncryptionProcessor; -import com.azure.cosmos.encryption.models.SqlQuerySpecWithEncryption; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -55,6 +60,8 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.function.Function; import java.util.stream.Collectors; @@ -193,9 +200,103 @@ public Mono> deleteItem(String itemId, PartitionKey partitionKey, CosmosItemRequestOptions requestOptions) { - return container.deleteItem(itemId, partitionKey, requestOptions); + this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync(); + return Mono.just(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(settings -> { + try { + return Mono.zip( + checkAndGetEncryptedId(itemId, settings), + checkAndGetEncryptedPartitionKey(partitionKey, settings) + ).flatMap(encryptedIdPartitionTuple -> container.deleteItem(encryptedIdPartitionTuple.getT1(), encryptedIdPartitionTuple.getT2(), requestOptions)); + } catch (Exception ex) { + return Mono.error(ex); + } + }); + } + + private Mono checkAndGetEncryptedId(String itemId, EncryptionSettings encryptionSettings) + { + if (this.encryptionProcessor.getClientEncryptionPolicy().getIncludedPaths().stream(). + anyMatch(includedPath -> includedPath.getPath().substring(1).equals(Constants.PROPERTY_NAME_ID))) { + return this.getEncryptedItem(encryptionSettings, Constants.PROPERTY_NAME_ID, itemId); + } + return Mono.just(itemId); } + private Mono checkAndGetEncryptedPartitionKey(PartitionKey partitionKey, EncryptionSettings encryptionSettings) { + if (encryptionSettings.getPartitionKeyPaths().isEmpty() || partitionKey == null) { + return Mono.just(partitionKey); + } + + JsonNode partitionKeyNode; + try { + partitionKeyNode = EncryptionUtils.getSimpleObjectMapper().readTree(partitionKey.toString()); + } catch (JsonProcessingException ex) { + return Mono.error(ex); + } + + if (partitionKeyNode.isArray()) { + ArrayNode arrayNode = (ArrayNode) partitionKeyNode; + + return Mono.just(new PartitionKeyBuilder()) + .flatMap(partitionKeyBuilder -> { + return Flux.fromIterable(encryptionSettings.getPartitionKeyPaths()) + .flatMap(path -> { + // case: partition key path is /a/b/c and the client encryption policy has /a in path. + // hence encrypt the partition key value with using its top level path /a since + // /c would have been encrypted in the document using /a's policy. + String partitionKeyPath = path.split("/")[0]; + + String childPartitionKey = arrayNode.elements().next().toString(); + if (this.encryptionProcessor.getClientEncryptionPolicy().getIncludedPaths().stream(). + anyMatch(includedPath -> includedPath.getPath().substring(1).equals(partitionKeyPath))) { + partitionKeyBuilder.add(childPartitionKey); + return Mono.empty(); + } + return getEncryptedItem(encryptionSettings, partitionKeyPath, childPartitionKey); + }) + .collectList() + .flatMap(encryptedItems -> { + for (String encryptedItem : encryptedItems) { + partitionKeyBuilder.add(encryptedItem); + } + return Mono.just(partitionKeyBuilder.build()); + }); + }); + } else { + return Mono.just((encryptionSettings.getPartitionKeyPaths().size() > 1)) + .flatMap(multiplePartitionPath -> { + if (multiplePartitionPath) { + return Mono.error(new MicrosoftDataEncryptionException("There should only be 1 PartitionKeyPath.")); + } + + return Mono.just(encryptionSettings.getPartitionKeyPaths().get(0)) + .flatMap(partitionKeyPath -> { + if (this.encryptionProcessor.getClientEncryptionPolicy().getIncludedPaths().stream(). + anyMatch(includedPath -> includedPath.getPath().substring(1).equals(partitionKeyPath))) { + return Mono.just(partitionKey); + } + return getEncryptedItem(encryptionSettings, partitionKeyPath, partitionKey.toString()); + }) + .map(encryptedPartitionKeyPath -> new PartitionKey(encryptedPartitionKeyPath)); + }); + } + } + + private Mono getEncryptedItem(EncryptionSettings encryptionSettings, String propertyName, String propertyValue) { + return encryptionSettings + .getEncryptionSettingForPropertyAsync(propertyName, this.encryptionProcessor) + .flatMap(settings -> { + try { + return Mono.just( + this.encryptionProcessor.encryptAndSerializeValue(settings, propertyValue, propertyName)); + } catch (MicrosoftDataEncryptionException ex) { + return Mono.error(ex); + } + }); + } + + /** * Deletes the item. *

@@ -225,10 +326,14 @@ public Mono> deleteItem(T item, CosmosItemRequest */ // TODO Make this api public once it is GA in cosmos core library Mono> deleteAllItemsByPartitionKey(PartitionKey partitionKey, CosmosItemRequestOptions requestOptions) { - if (requestOptions == null) { - requestOptions = new CosmosItemRequestOptions(); - } - return container.deleteAllItemsByPartitionKey(partitionKey, requestOptions); + Objects.requireNonNull(partitionKey, "partitionKey cannot be null"); + final CosmosItemRequestOptions options = Optional.ofNullable(requestOptions) + .orElse(new CosmosItemRequestOptions()); + + return this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptedSettings -> checkAndGetEncryptedPartitionKey(partitionKey, encryptedSettings)) + .flatMap(encryptedPartitionKey -> container.deleteAllItemsByPartitionKey(partitionKey, options)); } /** @@ -380,15 +485,23 @@ public Mono> readItem(String id, PartitionKey partitionKey, CosmosItemRequestOptions requestOptions, Class classType) { - if (requestOptions == null) { - requestOptions = new CosmosItemRequestOptions(); - } - - Mono> responseMessageMono = this.readItemHelper(id, partitionKey, requestOptions, false); - - return responseMessageMono.publishOn(encryptionScheduler).flatMap(cosmosItemResponse -> setByteArrayContent(cosmosItemResponse, - this.encryptionProcessor.decrypt(cosmosItemResponseBuilderAccessor.getByteArrayContent(cosmosItemResponse))) - .map(bytes -> this.responseFactory.createItemResponse(cosmosItemResponse, classType))); + final CosmosItemRequestOptions options = Optional.ofNullable(requestOptions) + .orElse(new CosmosItemRequestOptions()); + + return this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptedSettings -> Mono.zip( + checkAndGetEncryptedId(id, encryptedSettings), + checkAndGetEncryptedPartitionKey(partitionKey, encryptedSettings)) + .flatMap(encryptedIdPartitionKeyTuple -> + this.readItemHelper(encryptedIdPartitionKeyTuple.getT1(), + encryptedIdPartitionKeyTuple.getT2(), options, false)) + .publishOn(encryptionScheduler) + .flatMap(cosmosItemResponse -> setByteArrayContent( + cosmosItemResponse, + this.encryptionProcessor.decrypt(cosmosItemResponseBuilderAccessor + .getByteArrayContent(cosmosItemResponse)) + ).map(bytes -> this.responseFactory.createItemResponse(cosmosItemResponse, classType)))); } /** @@ -554,11 +667,15 @@ public Mono> patchItem( checkNotNull(partitionKey, "expected non-null partitionKey for patchItem"); checkNotNull(cosmosPatchOperations, "expected non-null cosmosPatchOperations"); - if (options == null) { - options = new CosmosPatchItemRequestOptions(); - } + final CosmosPatchItemRequestOptions patchOptions = Optional.ofNullable(options) + .orElse(new CosmosPatchItemRequestOptions()); - return patchItemHelper(itemId, partitionKey, cosmosPatchOperations, options, itemType); + return this.encryptionProcessor.initEncryptionSettingsIfNotInitializedAsync() + .thenReturn(this.encryptionProcessor.getEncryptionSettings()) + .flatMap(encryptionSettings -> Mono.zip( + checkAndGetEncryptedId(itemId, encryptionSettings), + checkAndGetEncryptedPartitionKey(partitionKey, encryptionSettings)) + .flatMap(encryptedIdPartitionKeyTuple -> patchItemHelper(encryptedIdPartitionKeyTuple.getT1(), encryptedIdPartitionKeyTuple.getT2(), cosmosPatchOperations, patchOptions, itemType))); } private Mono> patchItemHelper(String itemId, diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java index c06ae1b71f2ca..888752d3959f1 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/Constants.java @@ -15,4 +15,6 @@ public class Constants { public static final String ALLOW_CACHED_READS_HEADER = "x-ms-cosmos-allow-cachedreads"; public static final String DATABASE_RID_HEADER = "x-ms-cosmos-database-rid"; + + public static final String PROPERTY_NAME_ID = "id"; } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java index 42032b0f2947f..a10f3849cd206 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionProcessor.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.BooleanNode; import com.fasterxml.jackson.databind.node.DoubleNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; import com.fasterxml.jackson.databind.node.LongNode; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; @@ -41,6 +42,7 @@ import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -58,6 +60,7 @@ public class EncryptionProcessor { private ClientEncryptionPolicy clientEncryptionPolicy; private String containerRid; private String databaseRid; + private List partitionKeyPaths; private CosmosClientEncryptionKeyProperties cosmosClientEncryptionKeyProperties; private final EncryptionKeyStoreProviderImpl encryptionKeyStoreProviderImpl; private final static ImplementationBridgeHelpers.CosmosContainerPropertiesHelper.CosmosContainerPropertiesAccessor cosmosContainerPropertiesAccessor = ImplementationBridgeHelpers.CosmosContainerPropertiesHelper.getCosmosContainerPropertiesAccessor(); @@ -99,7 +102,15 @@ public Mono initializeEncryptionSettingsAsync(boolean isRetry) { { this.containerRid = cosmosContainerProperties.getResourceId(); this.databaseRid = cosmosContainerPropertiesAccessor.getSelfLink(cosmosContainerProperties).split("/")[1]; + + if (cosmosContainerProperties.getPartitionKeyDefinition().getPaths().isEmpty()) { + this.partitionKeyPaths = cosmosContainerProperties.getPartitionKeyDefinition().getPaths(); + } else { + this.partitionKeyPaths = new ArrayList<>(); + } + this.encryptionSettings.setDatabaseRid(this.databaseRid); + this.encryptionSettings.setPartitionKeyPaths(partitionKeyPaths); if (cosmosContainerProperties.getClientEncryptionPolicy() == null) { this.isEncryptionSettingsInitDone.set(true); return Mono.empty(); @@ -206,7 +217,7 @@ public Mono initEncryptionSettingsIfNotInitializedAsync() { return Mono.empty(); } - ClientEncryptionPolicy getClientEncryptionPolicy() { + public ClientEncryptionPolicy getClientEncryptionPolicy() { return clientEncryptionPolicy; } @@ -438,16 +449,37 @@ public void encryptAndSerializeProperty(EncryptionSettings encryptionSettings, J } } + public String encryptAndSerializeValue(EncryptionSettings encryptionSettings, String propertyValue, String propertyName) throws MicrosoftDataEncryptionException { + JsonNode propertyValueHolder = toJsonNode(propertyValue.getBytes(StandardCharsets.US_ASCII), TypeMarker.STRING); + return new String(encryptAndSerializeValue(encryptionSettings, null, propertyValueHolder, propertyName), StandardCharsets.US_ASCII); + } + public byte[] encryptAndSerializeValue(EncryptionSettings encryptionSettings, ObjectNode objectNode, JsonNode propertyValueHolder, String propertyName) throws MicrosoftDataEncryptionException { byte[] cipherText; byte[] cipherTextWithTypeMarker; + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + if (propertyValueHolder.getNodeType() != JsonNodeType.STRING) { + throw new IllegalArgumentException("Unsupported argument type. The value to escape has to be string " + + "type. Please refer to https://aka.ms/CosmosClientEncryption for more details."); + } + } Pair typeMarkerPair = toByteArray(propertyValueHolder); cipherText = encryptionSettings.getAeadAes256CbcHmac256EncryptionAlgorithm().encrypt(typeMarkerPair.getRight()); cipherTextWithTypeMarker = new byte[cipherText.length + 1]; cipherTextWithTypeMarker[0] = (byte) typeMarkerPair.getLeft().getValue(); System.arraycopy(cipherText, 0, cipherTextWithTypeMarker, 1, cipherText.length); + + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + // case: id does not support '/','\','?','#'. Convert Base64 string to Uri safe string + String base64UriSafeString = convertToBase64UriSafeString(cipherTextWithTypeMarker); + if (objectNode != null && !objectNode.isNull()) { + objectNode.put(propertyName, base64UriSafeString); + } + return base64UriSafeString.getBytes(StandardCharsets.UTF_8); + } + if (objectNode != null && !objectNode.isNull()) { objectNode.put(propertyName, cipherTextWithTypeMarker); } @@ -567,7 +599,15 @@ public JsonNode decryptAndSerializeValue(EncryptionSettings encryptionSettings, JsonNode propertyValueHolder, String propertyName) throws MicrosoftDataEncryptionException, IOException { byte[] cipherText; byte[] cipherTextWithTypeMarker; - cipherTextWithTypeMarker = propertyValueHolder.binaryValue(); + if (propertyName.equals(Constants.PROPERTY_NAME_ID)) { + if (propertyValueHolder.getNodeType() == JsonNodeType.NULL) { + return null; + } + cipherTextWithTypeMarker = convertFromBase64UriSafeString(propertyValueHolder.asText()); + } else { + cipherTextWithTypeMarker = propertyValueHolder.binaryValue(); + } + cipherText = new byte[cipherTextWithTypeMarker.length - 1]; System.arraycopy(cipherTextWithTypeMarker, 1, cipherText, 0, cipherTextWithTypeMarker.length - 1); @@ -627,6 +667,27 @@ public static JsonNode toJsonNode(byte[] serializedBytes, TypeMarker typeMarker) throw BridgeInternal.createCosmosException(0, "Invalid or Unsupported Data Type Passed " + typeMarker); } + private String convertToBase64UriSafeString(byte[] bytesToProcess) { + // Base 64 Encoding with URL and Filename Safe Alphabet https://datatracker.ietf.org/doc/html/rfc4648#section-5 + // https://docs.microsoft.com/en-us/azure/cosmos-db/concepts-limits#per-item-limits, due to base64 conversion and encryption + // the permissible size of the property will further reduce. + return Base64.getUrlEncoder().encodeToString(bytesToProcess); + } + + private byte[] convertFromBase64UriSafeString(String base64UriSafeString) { + return Base64.getUrlDecoder().decode(base64UriSafeString); + } + + private StringBuilder replaceString(StringBuilder sb, + String toReplace, + String replacement) { + int index = -1; + while ((index = sb.lastIndexOf(toReplace)) != -1) { + sb.replace(index, index + toReplace.length(), replacement); + } + return sb; + } + public enum TypeMarker { NULL(1), // not used BOOLEAN(2), diff --git a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java index 8c45d9be3e33e..e25c13be68e07 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/main/java/com/azure/cosmos/encryption/implementation/EncryptionSettings.java @@ -25,6 +25,7 @@ import java.security.InvalidKeyException; import java.time.Duration; import java.time.Instant; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -39,6 +40,7 @@ public final class EncryptionSettings { private EncryptionType encryptionType; private String databaseRid; private CosmosClientEncryptionKeyProperties cosmosClientEncryptionKeyProperties; + private List partitionKeyPaths; private final static EncryptionImplementationBridgeHelpers.CosmosEncryptionAsyncClientHelper.CosmosEncryptionAsyncClientAccessor cosmosEncryptionAsyncClientAccessor = EncryptionImplementationBridgeHelpers.CosmosEncryptionAsyncClientHelper.getCosmosEncryptionAsyncClientAccessor(); @@ -226,6 +228,14 @@ public void setEncryptionSettingForProperty(String propertyName, EncryptionSetti this.encryptionSettingCacheByPropertyName.set(propertyName, cachedEncryptionSettings); } + public List getPartitionKeyPaths() { + return partitionKeyPaths; + } + + public void setPartitionKeyPaths(List partitionKeyPaths) { + this.partitionKeyPaths = partitionKeyPaths; + } + static EncryptionSettings create( EncryptionSettings settingsForKey, EncryptionType encryptionType) throws MicrosoftDataEncryptionException { diff --git a/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/EncryptionCodeSnippet.java b/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/EncryptionCodeSnippet.java index 4eb0467d764c7..e8f9e2898701a 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/EncryptionCodeSnippet.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/EncryptionCodeSnippet.java @@ -124,7 +124,7 @@ void createContainerWithClientEncryptionPolicy(CosmosAsyncClient client) { paths.add(includedPath6); paths.add(includedPath7); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 2); String containerId = "myCol"; CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionPolicy); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/Program.java b/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/Program.java index 4c8a47ec840a7..9aad3fb512193 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/Program.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/Program.java @@ -155,7 +155,7 @@ private static void initialize(CosmosEncryptionAsyncClient cosmosEncryptionAsync CosmosContainerProperties containerProperties = new CosmosContainerProperties(Program.containerId, "/purchaseOrderNumber"); - containerProperties.setClientEncryptionPolicy(new ClientEncryptionPolicy(paths)); + containerProperties.setClientEncryptionPolicy(new ClientEncryptionPolicy(paths, 2)); cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().createContainer(containerProperties, ThroughputProperties.createManualThroughput(1000)).block(); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/ReadmeSamples.java b/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/ReadmeSamples.java index 3e9dbd3104c7d..c95291168c19c 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/ReadmeSamples.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/samples/java/com/azure/cosmos/encryption/ReadmeSamples.java @@ -86,7 +86,7 @@ public void createCosmosEncryptionContainer() { List paths = new ArrayList<>(); paths.add(includedPath); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 2); CosmosContainerProperties properties = new CosmosContainerProperties("", "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionPolicy); return cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().createContainer(properties); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java index efc84fa304dce..a2fb743d14fe1 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/CosmosEncryptionClientCachesTest.java @@ -68,7 +68,7 @@ public void before_CosmosItemTest() { CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName(), metadata2).block(); //Create collection with clientEncryptionPolicy - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(), 2); CosmosContainerProperties containerProperties = new CosmosContainerProperties("TestCollForEncryptionCacheTest" , "/mypk"); containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java index 58fe54893f845..db181101ba0b8 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/DotNetCompatibleTest.java @@ -70,7 +70,7 @@ public void before_CosmosItemTest() throws IOException { cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(cosmosAsyncDatabase.getId()); ClientEncryptionPolicy clientEncryptionPolicy = - new ClientEncryptionPolicy(getPaths()); + new ClientEncryptionPolicy(getPaths(), 2); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties containerProperties = new CosmosContainerProperties(containerId, "/mypk"); containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); @@ -100,12 +100,13 @@ public void createItemEncrypt_readItemDecrypt() throws IOException { this.cosmosEncryptionAsyncContainer.getCosmosAsyncContainer().createItem(dotNetEncryptedPocoJsonNode, partitionKey, requestOptions).block(); + JsonNode dotNetPOCOJsonNode = MAPPER.readTree(new File("src/test/resources/dotnetEncryption/POCO.json")); + partitionKey = new PartitionKey(dotNetPOCOJsonNode.get("mypk").asText()); //reading above saved .net encrypted json via java encryption library EncryptionPojo unencryptedPojo = - this.cosmosEncryptionAsyncContainer.readItem(dotNetEncryptedPocoJsonNode.get("id").asText(), partitionKey + this.cosmosEncryptionAsyncContainer.readItem(dotNetPOCOJsonNode.get("id").asText(), partitionKey , new CosmosItemRequestOptions(), EncryptionPojo.class).block().getItem(); - JsonNode dotNetPOCOJsonNode = MAPPER.readTree(new File("src/test/resources/dotnetEncryption/POCO.json")); EncryptionPojo unencryptedPoco = MAPPER.treeToValue(dotNetPOCOJsonNode, EncryptionPojo.class); //validating java decrypted pojo similar to original .net unencrypted poco diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java index a5415f557db45..ba2702015ac0b 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionAsyncApiCrudTest.java @@ -57,6 +57,7 @@ public class EncryptionAsyncApiCrudTest extends TestSuiteBase { private CosmosAsyncClient client; private CosmosEncryptionAsyncClient cosmosEncryptionAsyncClient; private CosmosEncryptionAsyncContainer encryptionContainerWithIncompatiblePolicyVersion; + private CosmosContainerProperties propertiesWithIdAndPartitionKeyPath; CosmosEncryptionAsyncContainer cosmosEncryptionAsyncContainer; CosmosEncryptionAsyncDatabase cosmosEncryptionAsyncDatabase; @@ -76,12 +77,12 @@ public void before_CosmosItemTest() { cosmosEncryptionAsyncDatabase = getSharedEncryptionDatabase(cosmosEncryptionAsyncClient); cosmosEncryptionAsyncContainer = getSharedEncryptionContainer(cosmosEncryptionAsyncClient); - ClientEncryptionPolicy clientEncryptionWithPolicyFormatVersion2 = new ClientEncryptionPolicy(getPaths()); - ReflectionUtils.setPolicyFormatVersion(clientEncryptionWithPolicyFormatVersion2, 2); + ClientEncryptionPolicy clientEncryptionWithPolicyFormatVersion2 = new ClientEncryptionPolicy(getPaths(), 2); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionWithPolicyFormatVersion2); cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().createContainer(properties).block(); + encryptionContainerWithIncompatiblePolicyVersion = cosmosEncryptionAsyncDatabase.getCosmosEncryptionAsyncContainer(containerId); } @@ -95,6 +96,7 @@ public void afterClass() { @Test(groups = {"encryption"}, timeOut = TIMEOUT) public void createItemEncrypt_readItemDecrypt() { EncryptionPojo properties = getItem(UUID.randomUUID().toString()); + CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties, new PartitionKey(properties.getMypk()), new CosmosItemRequestOptions()).block(); assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); @@ -365,7 +367,7 @@ public void crudQueryStaleCache() { cosmosEncryptionAsyncClient.getCosmosEncryptionAsyncDatabase(asyncClient.getDatabase(databaseId)); String containerId = UUID.randomUUID().toString(); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(), 2); createEncryptionContainer(cosmosEncryptionAsyncDatabase, clientEncryptionPolicy, containerId); CosmosEncryptionAsyncContainer encryptionAsyncContainerOriginal = cosmosEncryptionAsyncDatabase.getCosmosEncryptionAsyncContainer(containerId); @@ -398,7 +400,7 @@ public void crudQueryStaleCache() { //Deleting and creating container encryptionAsyncContainerOriginal.getCosmosAsyncContainer().delete().block(); - ClientEncryptionPolicy policyWithOneEncryptionPolicy = new ClientEncryptionPolicy(getPathWithOneEncryptionField()); + ClientEncryptionPolicy policyWithOneEncryptionPolicy = new ClientEncryptionPolicy(getPathWithOneEncryptionField(), 2); createEncryptionContainer(cosmosEncryptionAsyncDatabase, policyWithOneEncryptionPolicy, containerId); CosmosEncryptionAsyncContainer encryptionAsyncContainerNew = getNewEncryptionContainerProxyObject(cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().getId(), containerId); encryptionAsyncContainerNew.createItem(encryptionPojo, @@ -1011,6 +1013,109 @@ public void crudOnDifferentOverload() { assertThat(deleteResponse3.getStatusCode()).isEqualTo(200); } +// @Test(groups = {"encryption"}, timeOut = TIMEOUT) +// public void crudOperationsWithIdAndPartitionKeyEncrypted() { +// cosmosEncryptionAsyncDatabase.getCosmosAsyncDatabase().createContainer(propertiesWithIdAndPartitionKeyPath).block(); +// List actualProperties = new ArrayList<>(); +// // Read item +// EncryptionPojo properties = getItem(UUID.randomUUID().toString()); +// CosmosItemResponse itemResponse = cosmosEncryptionAsyncContainer.createItem(properties).block(); +// assertThat(itemResponse.getRequestCharge()).isGreaterThan(0); +// EncryptionPojo responseItem = itemResponse.getItem(); +// validateResponse(properties, responseItem); +// actualProperties.add(properties); +// +// properties = getItem(UUID.randomUUID().toString()); +// CosmosItemResponse itemResponse1 = cosmosEncryptionAsyncContainer.createItem(properties, new CosmosItemRequestOptions()).block(); +// assertThat(itemResponse1.getRequestCharge()).isGreaterThan(0); +// EncryptionPojo responseItem1 = itemResponse1.getItem(); +// validateResponse(properties, responseItem1); +// actualProperties.add(properties); +// +// //Upsert Item +// properties = getItem(UUID.randomUUID().toString()); +// CosmosItemResponse upsertResponse1 = cosmosEncryptionAsyncContainer.upsertItem(properties).block(); +// assertThat(upsertResponse1.getRequestCharge()).isGreaterThan(0); +// EncryptionPojo responseItem2 = upsertResponse1.getItem(); +// validateResponse(properties, responseItem2); +// actualProperties.add(properties); +// +// properties = getItem(UUID.randomUUID().toString()); +// CosmosItemResponse upsertResponse2 = cosmosEncryptionAsyncContainer.upsertItem(properties, new CosmosItemRequestOptions()).block(); +// assertThat(upsertResponse2.getRequestCharge()).isGreaterThan(0); +// EncryptionPojo responseItem3 = upsertResponse2.getItem(); +// validateResponse(properties, responseItem3); +// actualProperties.add(properties); +// +// //Read Item +// EncryptionPojo readItem = cosmosEncryptionAsyncContainer.readItem(actualProperties.get(0).getId(), +// new PartitionKey(actualProperties.get(0).getMypk()), EncryptionPojo.class).block().getItem(); +// validateResponse(actualProperties.get(0), readItem); +// +// //Query Item +// String query = String.format("SELECT * from c where c.id = '%s'", actualProperties.get(1).getId()); +// +// CosmosPagedFlux feedResponseIterator = +// cosmosEncryptionAsyncContainer.queryItems(query, EncryptionPojo.class); +// List feedResponse = feedResponseIterator.byPage().blockFirst().getResults(); +// assertThat(feedResponse.size()).isGreaterThanOrEqualTo(1); +// for (EncryptionPojo pojo : feedResponse) { +// if (pojo.getId().equals(actualProperties.get(1).getId())) { +// validateResponse(pojo, responseItem1); +// } +// } +// +// CosmosQueryRequestOptions cosmosQueryRequestOptions1 = new CosmosQueryRequestOptions(); +// +// CosmosPagedFlux feedResponseIterator1 = +// cosmosEncryptionAsyncContainer.queryItems(query, cosmosQueryRequestOptions1, EncryptionPojo.class); +// List feedResponse1 = feedResponseIterator1.byPage().blockFirst().getResults(); +// assertThat(feedResponse1.size()).isGreaterThanOrEqualTo(1); +// for (EncryptionPojo pojo : feedResponse1) { +// if (pojo.getId().equals(actualProperties.get(1).getId())) { +// validateResponse(pojo, responseItem1); +// } +// } +// +// CosmosQueryRequestOptions cosmosQueryRequestOptions2 = new CosmosQueryRequestOptions(); +// SqlQuerySpec querySpec = new SqlQuerySpec(query); +// +// CosmosPagedFlux feedResponseIterator2 = +// cosmosEncryptionAsyncContainer.queryItems(querySpec, cosmosQueryRequestOptions2, EncryptionPojo.class); +// List feedResponse2 = feedResponseIterator2.byPage().blockFirst().getResults(); +// assertThat(feedResponse2.size()).isGreaterThanOrEqualTo(1); +// for (EncryptionPojo pojo : feedResponse2) { +// if (pojo.getId().equals(actualProperties.get(1).getId())) { +// validateResponse(pojo, responseItem1); +// } +// } +// +// //Replace Item +// CosmosItemResponse replaceResponse = +// cosmosEncryptionAsyncContainer.replaceItem(actualProperties.get(2), actualProperties.get(2).getId(), +// new PartitionKey(actualProperties.get(2).getMypk())).block(); +// assertThat(upsertResponse1.getRequestCharge()).isGreaterThan(0); +// responseItem = replaceResponse.getItem(); +// validateResponse(actualProperties.get(2), responseItem); +// +// //Delete Item +// CosmosItemResponse deleteResponse = cosmosEncryptionAsyncContainer.deleteItem(actualProperties.get(0).getId(), +// new PartitionKey(actualProperties.get(0).getMypk())).block(); +// assertThat(deleteResponse.getStatusCode()).isEqualTo(204); +// +// CosmosItemResponse deleteResponse1 = cosmosEncryptionAsyncContainer.deleteItem(actualProperties.get(1).getId(), +// new PartitionKey(actualProperties.get(1).getMypk()), new CosmosItemRequestOptions()).block(); +// assertThat(deleteResponse1.getStatusCode()).isEqualTo(204); +// +// CosmosItemResponse deleteResponse2 = cosmosEncryptionAsyncContainer.deleteItem(actualProperties.get(2), +// new CosmosItemRequestOptions()).block(); +// assertThat(deleteResponse2.getStatusCode()).isEqualTo(204); +// +// CosmosItemResponse deleteResponse3 = cosmosEncryptionAsyncContainer.deleteAllItemsByPartitionKey(new PartitionKey(actualProperties.get(3).getMypk()), +// new CosmosItemRequestOptions()).block(); +// assertThat(deleteResponse3.getStatusCode()).isEqualTo(200); +// } + static void validateResponseWithOneFieldEncryption(EncryptionPojo originalItem, EncryptionPojo result) { assertThat(result.getId()).isEqualTo(originalItem.getId()); assertThat(result.getNonSensitive()).isEqualTo(originalItem.getNonSensitive()); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java index 497487b8deeb8..8b670a266048c 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/EncryptionCosmosEncryptionChangeFeedTest.java @@ -264,7 +264,7 @@ private CosmosAsyncContainer createLeaseCollection(int provisionedThroughput) { } private CosmosEncryptionAsyncContainer createFeedCollection() { - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(), 2); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionPolicy); diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java index 3d09303f79bfc..16077a9bba142 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/TestSuiteBase.java @@ -240,7 +240,7 @@ public static void beforeSuite() { SHARED_ENCRYPTION_DATABASE.createClientEncryptionKey("key2", CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName(), metadata2).block(); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths()); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(getPaths(),2); String containerId = UUID.randomUUID().toString(); CosmosContainerProperties properties = new CosmosContainerProperties(containerId, "/mypk"); properties.setClientEncryptionPolicy(clientEncryptionPolicy); @@ -1292,6 +1292,18 @@ protected static List getPaths() { includedPath13.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.getName()); includedPath13.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); + ClientEncryptionIncludedPath includedPath14 = new ClientEncryptionIncludedPath(); + includedPath14.setClientEncryptionKeyId("key1"); + includedPath14.setPath("/id"); + includedPath14.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.getName()); + includedPath14.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); + + ClientEncryptionIncludedPath includedPath15 = new ClientEncryptionIncludedPath(); + includedPath15.setClientEncryptionKeyId("key1"); + includedPath15.setPath("/pk"); + includedPath15.setEncryptionType(CosmosEncryptionType.DETERMINISTIC.getName()); + includedPath15.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); + List paths = new ArrayList<>(); paths.add(includedPath1); paths.add(includedPath2); @@ -1306,6 +1318,8 @@ protected static List getPaths() { paths.add(includedPath11); paths.add(includedPath12); paths.add(includedPath13); + paths.add(includedPath14); + paths.add(includedPath15); return paths; } diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/implementation/EncryptionProcessorAndSettingsTest.java b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/implementation/EncryptionProcessorAndSettingsTest.java index 066ae900e80fb..18913e31c1790 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/implementation/EncryptionProcessorAndSettingsTest.java +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/java/com/azure/cosmos/encryption/implementation/EncryptionProcessorAndSettingsTest.java @@ -206,7 +206,7 @@ private ClientEncryptionPolicy generateClientEncryptionPolicy() { includedPath1.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); List paths = new ArrayList<>(); paths.add(includedPath1); - return new ClientEncryptionPolicy(paths); + return new ClientEncryptionPolicy(paths, 1); } private CosmosContainerProperties generateContainerWithCosmosEncryptionPolicy() { @@ -219,7 +219,7 @@ private CosmosContainerProperties generateContainerWithCosmosEncryptionPolicy() includedPath1.setEncryptionAlgorithm(CosmosEncryptionAlgorithm.AEAD_AES_256_CBC_HMAC_SHA256.getName()); List paths = new ArrayList<>(); paths.add(includedPath1); - return containerProperties.setClientEncryptionPolicy(new ClientEncryptionPolicy(paths)); + return containerProperties.setClientEncryptionPolicy(new ClientEncryptionPolicy(paths, 1)); } private CosmosClientEncryptionKeyProperties generateClientEncryptionKeyProperties() throws JsonProcessingException { diff --git a/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json b/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json index 448e61486361c..7f1e2684fbcdc 100644 --- a/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json +++ b/sdk/cosmos/azure-cosmos-encryption/src/test/resources/dotnetEncryption/EncryptedPOCO.json @@ -1,20 +1,20 @@ { - "id": "7d925bf9-d583-4c6d-a49f-137bee7cff91", + "id": "BQF1OMHlXj-zDxKasrXQFwusq5lZf-AjwlS5Z4ZFdtljRO9ljKsPhPdZ4bGsXYbgtO4WrsHdqVy7r_EB101kO7xfjmGMvTOBiRt_M80AcNwiwJ1vFX9-uLudp3ttD-wKyZs=", "mypk": "7d925bf9-d583-4c6d-a49f-137bee7cff91", "nonSensitive": "cf012e6a-3dd1-4f1a-bd93-dc2b7fee5e97", "sensitiveString": "BQE8sgzM2aDfOlOiU9tYsgR5CghB/WZ/jsluf4HoYotfeUBqDBn3hVfx3A+o4m2mm5dEvnJyOR2Sl+NZezsYN71d", "sensitiveInt": "BAF+h8xlYiIqaH9TmN8/mgzX8ulodNhbYPASSPEGIVb+3qTR8J4qo/jZoz/qQdkxkEv8TV/aj0UdPEpGF4EBe+88", - "sensitiveFloat": "AwHBSYf0AqKfOX60Ppn/yj9ycfuFlOWEHGQ230BypHacyeBX/o3jRmlgnRNJuZpr1I9n0pNQl1yCfJkscMUVs8ao", + "sensitiveFloat": "BAFjPNt6v/gmO/v1uZCeTPgT6Tioja+rOzr1I8OK3hjC6tU6eZlXk0TUBVtu6ZkbIMo+cMAxH7IVC5diCnJfkjlS", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", - "sensitiveDouble": "AwHu2zdLESFrjQ7L9KbCenSeEXHOXKjV9d2UCfhNhR/ImYmODXymlKGxapHhzmUabdEp6hKP6ZNIjqJ5xvFD6EYs", + "sensitiveDouble": "AwHMrObWBev4hnUulDL5dvLxybF22O3VzY7//PXyano3Hrl8A967TAp3fdRATdaFHydATozMzgREtbhjJNk1WT1d", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", "sensitiveNestedPojo": { - "id": "BQERuT+l/kKc51OtABiPG3/lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj+Ue4B0oY0OYbKCuOYF3wLctg7P0cO", + "id": "BQERuT-l_kKc51OtABiPG3_lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj-Ue4B0oY0OYbKCuOYF3wLctg7P0cO", "mypk": "BQERuT+l/kKc51OtABiPG3/lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj+Ue4B0oY0OYbKCuOYF3wLctg7P0cO", "nonSensitive": null, "sensitiveString": "BQERuT+l/kKc51OtABiPG3/lu8hpIJNGwUgJVzr71dKJk9wcuuzZbMqtsxj+Ue4B0oY0OYbKCuOYF3wLctg7P0cO", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -82,12 +82,12 @@ "sensitiveChildPojo2DArray": [ [ { - "id": "BQHRkzWrw6w88Rak29a/mWMh57hvictUtZEK0YuYV+LNRuvCBPU74+btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", + "id": "BQHRkzWrw6w88Rak29a_mWMh57hvictUtZEK0YuYV-LNRuvCBPU74-btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", "mypk": null, "nonSensitive": null, "sensitiveString": "BQFfH6UGQgHuCZ1SKb8uDdEEdpjZ9+oFgKXDZGR5awXt5hY7B0D5i+6WQQNp2y2Vwsql32ZhCGEyg2vpew/heajPaIqKEPCCklz0WiVc/lrePg==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -123,12 +123,12 @@ "sensitiveChildPojoList": null }, { - "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C/wEauxKAYYswqUJ6DWd5eiIsFB/bjGViIq5A", + "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C_wEauxKAYYswqUJ6DWd5eiIsFB_bjGViIq5A", "mypk": null, "nonSensitive": null, "sensitiveString": "BQG3BuavLLquGcjrQw50+NLMm/86/+qVs+75b0jVTcfgjdkwXpTyFY7bRb/SSvxb7DYG7/za32PJ9Q5L4t5d3jvluLs2JWe6wb0fzjw5HSVuEA==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -142,12 +142,12 @@ ], [ { - "id": "BQHRkzWrw6w88Rak29a/mWMh57hvictUtZEK0YuYV+LNRuvCBPU74+btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", + "id": "BQHRkzWrw6w88Rak29a_mWMh57hvictUtZEK0YuYV-LNRuvCBPU74-btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", "mypk": null, "nonSensitive": null, "sensitiveString": "BQFfH6UGQgHuCZ1SKb8uDdEEdpjZ9+oFgKXDZGR5awXt5hY7B0D5i+6WQQNp2y2Vwsql32ZhCGEyg2vpew/heajPaIqKEPCCklz0WiVc/lrePg==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -183,12 +183,12 @@ "sensitiveChildPojoList": null }, { - "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C/wEauxKAYYswqUJ6DWd5eiIsFB/bjGViIq5A", + "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C_wEauxKAYYswqUJ6DWd5eiIsFB_bjGViIq5A", "mypk": null, "nonSensitive": null, "sensitiveString": "BQG3BuavLLquGcjrQw50+NLMm/86/+qVs+75b0jVTcfgjdkwXpTyFY7bRb/SSvxb7DYG7/za32PJ9Q5L4t5d3jvluLs2JWe6wb0fzjw5HSVuEA==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -203,12 +203,12 @@ ], "sensitiveChildPojoList": [ { - "id": "BQHRkzWrw6w88Rak29a/mWMh57hvictUtZEK0YuYV+LNRuvCBPU74+btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", + "id": "BQHRkzWrw6w88Rak29a_mWMh57hvictUtZEK0YuYV-LNRuvCBPU74-btLrxseJ4VZ3zesySbaBGkICzT7zq44M1j", "mypk": null, "nonSensitive": null, "sensitiveString": "BQFfH6UGQgHuCZ1SKb8uDdEEdpjZ9+oFgKXDZGR5awXt5hY7B0D5i+6WQQNp2y2Vwsql32ZhCGEyg2vpew/heajPaIqKEPCCklz0WiVc/lrePg==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -244,12 +244,12 @@ "sensitiveChildPojoList": null }, { - "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C/wEauxKAYYswqUJ6DWd5eiIsFB/bjGViIq5A", + "id": "BQEUkOOtKpvfp4tbzdsgHToSUv8fUstDi025Q6n6oOvi5mFelP1C_wEauxKAYYswqUJ6DWd5eiIsFB_bjGViIq5A", "mypk": null, "nonSensitive": null, "sensitiveString": "BQG3BuavLLquGcjrQw50+NLMm/86/+qVs+75b0jVTcfgjdkwXpTyFY7bRb/SSvxb7DYG7/za32PJ9Q5L4t5d3jvluLs2JWe6wb0fzjw5HSVuEA==", "sensitiveInt": "BAFZBTqQK+9Z7UC1W1NBh+LHLzAEcfFgCc53BD6EtcfGxLZXjKm/F9OFNB+gMeM3T4SFj6jG5DH6TtjVh1XRum4/", - "sensitiveFloat": "AwHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", + "sensitiveFloat": "BAHRoUMnYT23ocPkUoERjaSVcWxN05sjBivH0Dt4S5rC92YyZoyZokxGa5wnfpKuK49wr9wPynxF5QurRIha2d+c", "sensitiveLong": "BAG8flsdV8r870XeoPwTErbRBPCU5wdoAPSICreKQigwYblfexcEAosleZ+FO261u/m8X97ObH5m8hkxEuQzPURL", "sensitiveDouble": "AwHAQXtks16DzqIkLNWYZ7TWcjNCLPIYdiTe3NG7hXqg+DJirv5y2hx/fPiGuSKQ30ocEKkiqnxwISegmxZQt1De", "sensitiveBoolean": "AgHOBKDUuHWgivJCu+I8JgIqI3CuSgNKYKpn6nNUN8nqtyPkwnOJXYIeKpXS55/UzzaMAfRd1CZbT2oHdkIzUfuN", @@ -261,9 +261,9 @@ "sensitiveChildPojoList": null } ], - "_rid": "-dspAMVPSPMBAAAAAAAAAA==", - "_self": "dbs/-dspAA==/colls/-dspAMVPSPM=/docs/-dspAMVPSPMBAAAAAAAAAA==/", - "_etag": "00000000-0000-0000-984f-74dd1e6e01d7", + "_rid": "frBRAOuISWIBAAAAAAAAAA==", + "_self": "dbs/frBRAA==/colls/frBRAOuISWI=/docs/frBRAOuISWIBAAAAAAAAAA==/", + "_etag": "\"01094fbb-0000-0300-0000-63f528970000\"", "_attachments": "attachments/", - "_ts": 1629744506 + "_ts": 1677011095 } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java index 9e58f9da1b991..5ea5efe4ac4e0 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/ClientEncryptionPolicy.java @@ -7,7 +7,6 @@ import com.azure.cosmos.implementation.JsonSerializable; import com.azure.cosmos.implementation.apachecommons.lang.StringUtils; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.databind.node.ObjectNode; import java.util.ArrayList; import java.util.List; @@ -35,40 +34,40 @@ public final class ClientEncryptionPolicy { * Constructor. * * @param paths list of path of the item that need encryption along with path-specific settings. + * the PolicyFormatVersion will be set to 1 which is the default value. + * Note: If you need to include partition key or id field paths as part of the ClientEncryptionPolicy, please set PolicyFormatVersion to 2. */ public ClientEncryptionPolicy(List paths) { - this.validateIncludedPaths(paths); - this.includedPaths = paths; this.policyFormatVersion = 1; - } - - /** - * Constructor. - */ - public ClientEncryptionPolicy() { - this.jsonSerializable = new JsonSerializable(); + validateIncludedPaths(paths, policyFormatVersion); + this.includedPaths = paths; } /** * Constructor. * - * @param jsonString the json string that represents the client encryption policy. + * @param paths list of path of the item that need encryption along with path-specific settings. + * @param policyFormatVersion version of the client encryption policy definition. Current supported versions are 1 and 2. Default version is 1. + * Note: If you need to include partition key or id field paths as part of the ClientEncryptionPolicy, please set PolicyFormatVersion to 2. */ - ClientEncryptionPolicy(String jsonString) { - this.jsonSerializable = new JsonSerializable(jsonString); + public ClientEncryptionPolicy(List paths, int policyFormatVersion) { + if (policyFormatVersion > 2 || policyFormatVersion < 1) { + throw new IllegalArgumentException("Supported versions of client encryption policy are 1 and 2."); + } + this.policyFormatVersion = policyFormatVersion; + validateIncludedPaths(paths, policyFormatVersion); + this.includedPaths = paths; } /** * Constructor. - * - * @param objectNode the object node that represents the client encryption policy. */ - ClientEncryptionPolicy(ObjectNode objectNode) { - this.jsonSerializable = new JsonSerializable(objectNode); + public ClientEncryptionPolicy() { + this.jsonSerializable = new JsonSerializable(); } /** - * Gets the list of path of the item that need encryption along with path-specific settings. + * Gets the list of paths of the item that need encryption along with path-specific settings. * @return includedPaths */ public List getIncludedPaths() { @@ -83,28 +82,46 @@ public int getPolicyFormatVersion() { return policyFormatVersion; } - void validatePartitionKeyPathsAreNotEncrypted(List> partitionKeyPathTokens) { + /** + * Ensures that partition key paths specified in the client encryption policy for encryption are encrypted using Deterministic encryption algorithm. + * @param partitionKeyPathTokens Tokens corresponding to validated partition key. + */ + void validatePartitionKeyPathsIfEncrypted(List> partitionKeyPathTokens) { checkNotNull(partitionKeyPathTokens, "partitionKeyPathTokens cannot be null"); - List propertiesToEncrypt = - this.includedPaths.stream().map(clientEncryptionIncludedPath -> clientEncryptionIncludedPath.getPath().substring(1)).collect(Collectors.toList()); + for (List tokensInPath : partitionKeyPathTokens) { checkNotNull(tokensInPath); if (tokensInPath.size() > 0) { String topLevelToken = tokensInPath.get(0); - if (propertiesToEncrypt.contains(topLevelToken)) { - throw new IllegalArgumentException(String.format("Path %s which is part of the partition key " + - "cannot be included" + - " in the ClientEncryptionPolicy.", topLevelToken)); + + // paths in included paths start with "/". Get the ClientEncryptionIncludedPath and validate. + List encrypterPartitionKeyPath = + this.includedPaths.stream().filter(clientEncryptionIncludedPath -> clientEncryptionIncludedPath.getPath().substring(1).equals(topLevelToken)).collect(Collectors.toList()); + + if (encrypterPartitionKeyPath.size() >0) { + if (this.policyFormatVersion < 2) { + throw new IllegalArgumentException(String.format("Path %s which is part of the partition key " + + "cannot be encrypted" + + " with PolicyFormatVersion %s. Please use PolicyFormatVersion 2.", topLevelToken, policyFormatVersion)); + } + + // for the ClientEncryptionIncludedPath found check the encryption type. + if (!encrypterPartitionKeyPath.stream().map(encrypter -> encrypter.getEncryptionType()).findFirst().orElse(null).equals(Constants.Properties.DETERMINISTIC)) { + throw new IllegalArgumentException(String.format("Path %s which is part of the partition key " + + "has to be encrypted" + + " with Deterministic type Encryption.", topLevelToken)); + } + } } } } - private void validateIncludedPaths(List clientEncryptionIncludedPath) { + private static void validateIncludedPaths(List clientEncryptionIncludedPath, int policyFormatVersion) { List includedPathsList = new ArrayList<>(); for (ClientEncryptionIncludedPath path : clientEncryptionIncludedPath) { - this.validateClientEncryptionIncludedPath(path); + validateClientEncryptionIncludedPath(path, policyFormatVersion); if (includedPathsList.contains(path.getPath())) { throw new IllegalArgumentException("Duplicate Path found in clientEncryptionIncludedPath."); } @@ -113,7 +130,7 @@ private void validateIncludedPaths(List clientEncr } } - private void validateClientEncryptionIncludedPath(ClientEncryptionIncludedPath clientEncryptionIncludedPath) { + private static void validateClientEncryptionIncludedPath(ClientEncryptionIncludedPath clientEncryptionIncludedPath, int policyFormatVersion) { if (clientEncryptionIncludedPath == null) { throw new IllegalArgumentException("clientEncryptionIncludedPath is null"); } @@ -123,11 +140,20 @@ private void validateClientEncryptionIncludedPath(ClientEncryptionIncludedPath c } if (clientEncryptionIncludedPath.getPath().charAt(0) != '/' - || clientEncryptionIncludedPath.getPath().lastIndexOf('/') != 0 - || clientEncryptionIncludedPath.getPath().substring(1).equals("id")) { + || clientEncryptionIncludedPath.getPath().lastIndexOf('/') != 0) { throw new IllegalArgumentException("Invalid path " + clientEncryptionIncludedPath.getPath()); } + if (clientEncryptionIncludedPath.getPath().substring(1).equals(Constants.Properties.ID)) { + if (policyFormatVersion < 2) { + throw new IllegalArgumentException(String.format("Path %s cannot be encrypted with policyFormatVersion %s.", clientEncryptionIncludedPath.getPath(), policyFormatVersion)); + } + + if (!clientEncryptionIncludedPath.getEncryptionType().equals(Constants.Properties.DETERMINISTIC)) { + throw new IllegalArgumentException(String.format("Only deterministic encryption type is supported for path %s.", clientEncryptionIncludedPath.getPath())); + } + } + if (StringUtils.isEmpty(clientEncryptionIncludedPath.getClientEncryptionKeyId())) { throw new IllegalArgumentException("clientEncryptionKeyId in clientEncryptionIncludedPath is empty"); } diff --git a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java index 78675b6e29ffd..565eb6a367908 100644 --- a/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java +++ b/sdk/cosmos/azure-cosmos/src/main/java/com/azure/cosmos/models/CosmosContainerProperties.java @@ -126,7 +126,7 @@ public PartitionKeyDefinition getPartitionKeyDefinition() { public CosmosContainerProperties setPartitionKeyDefinition(PartitionKeyDefinition partitionKeyDefinition) { this.documentCollection.setPartitionKey(partitionKeyDefinition); if (this.getClientEncryptionPolicy() != null) { - this.getClientEncryptionPolicy().validatePartitionKeyPathsAreNotEncrypted(this.getPartitionKeyPathTokensList()); + this.getClientEncryptionPolicy().validatePartitionKeyPathsIfEncrypted(this.getPartitionKeyPathTokensList()); } return this; @@ -315,7 +315,7 @@ public ClientEncryptionPolicy getClientEncryptionPolicy() { */ public CosmosContainerProperties setClientEncryptionPolicy(ClientEncryptionPolicy value) { if (value != null) { - value.validatePartitionKeyPathsAreNotEncrypted(this.getPartitionKeyPathTokensList()); + value.validatePartitionKeyPathsIfEncrypted(this.getPartitionKeyPathTokensList()); } this.documentCollection.setClientEncryptionPolicy(value); diff --git a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosContainerTest.java b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosContainerTest.java index 36ce99c42b75e..3a12249a7559f 100644 --- a/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosContainerTest.java +++ b/sdk/cosmos/azure-cosmos/src/test/java/com/azure/cosmos/CosmosContainerTest.java @@ -119,16 +119,23 @@ public void createContainer_withEncryption() { path1.setClientEncryptionKeyId("containerTestKey1"); ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); - path2.setPath("/path2"); + path2.setPath("/mypk"); path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); path2.setEncryptionType("Deterministic"); path2.setClientEncryptionKeyId("containerTestKey2"); + ClientEncryptionIncludedPath path3 = new ClientEncryptionIncludedPath(); + path3.setPath("/id"); + path3.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path3.setEncryptionType("Deterministic"); + path3.setClientEncryptionKeyId("containerTestKey2"); + List paths = new ArrayList<>(); paths.add(path1); paths.add(path2); + paths.add(path3); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths,2); containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); CosmosContainerResponse containerResponse = createdDatabase.createContainer(containerProperties); @@ -137,7 +144,111 @@ public void createContainer_withEncryption() { } @Test(groups = {"emulator"}, timeOut = TIMEOUT) - public void createContainer_withPartitionKeyInEncryption() { + public void createContainer_withPartitionKeyWrongPolicyFormatVersion() { + String collectionName = UUID.randomUUID().toString(); + CosmosContainerProperties containerProperties = getCollectionDefinition(collectionName); + + ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath(); + path1.setPath("/mypk"); + path1.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path1.setEncryptionType("Deterministic"); + path1.setClientEncryptionKeyId("containerTestKey1"); + + ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); + path2.setPath("/path2"); + path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path2.setEncryptionType("Deterministic"); + path2.setClientEncryptionKeyId("containerTestKey2"); + + List paths = new ArrayList<>(); + paths.add(path1); + paths.add(path2); + + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 1); + CosmosContainerResponse containerResponse = null; + + //Verify partition key in CosmosContainerProperties constructor with encrypted field. + try { + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + containerResponse = createdDatabase.createContainer(containerProperties); + fail("createContainer should fail as mypk which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be encrypted with PolicyFormatVersion 1. Please use PolicyFormatVersion 2."); + } + + + //Verify for composite key + collectionName = UUID.randomUUID().toString(); + containerProperties = new CosmosContainerProperties(collectionName, "/mypk/mypk1"); + try { + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + containerResponse = createdDatabase.createContainer(containerProperties); + fail("createContainer should fail as mypk which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be encrypted with PolicyFormatVersion 1. Please use PolicyFormatVersion 2."); + } + + + //Verify setPartitionKeyDefinition with encrypted field. + collectionName = UUID.randomUUID().toString(); + containerProperties = new CosmosContainerProperties(collectionName, "/differentKey"); + try { + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + PartitionKeyDefinition partitionKeyDefinition = new PartitionKeyDefinition(); + List keyPaths = new ArrayList<>(); + keyPaths.add("/mypk"); + partitionKeyDefinition.setPaths(keyPaths); + containerProperties.setPartitionKeyDefinition(partitionKeyDefinition); + containerResponse = createdDatabase.createContainer(containerProperties); + fail("createContainer should fail as mypk which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be encrypted with PolicyFormatVersion 1. Please use PolicyFormatVersion 2."); + } + + //This should pass as we check only the first key of the composite key. + collectionName = UUID.randomUUID().toString(); + containerProperties = new CosmosContainerProperties(collectionName, "/mypk1/mypk"); + containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); + containerResponse = createdDatabase.createContainer(containerProperties); + assertThat(containerResponse.getRequestCharge()).isGreaterThan(0); + validateContainerResponseWithEncryption(containerProperties, containerResponse, clientEncryptionPolicy); + } + + @Test(groups = {"emulator"}, timeOut = TIMEOUT) + public void createEncryptionPolicy_withIdWrongPolicyFormatVersion() { + String collectionName = UUID.randomUUID().toString(); + CosmosContainerProperties containerProperties = getCollectionDefinition(collectionName); + + ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath(); + path1.setPath("/id"); + path1.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path1.setEncryptionType("Deterministic"); + path1.setClientEncryptionKeyId("containerTestKey1"); + + ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); + path2.setPath("/path2"); + path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path2.setEncryptionType("Deterministic"); + path2.setClientEncryptionKeyId("containerTestKey2"); + + List paths = new ArrayList<>(); + paths.add(path1); + paths.add(path2); + + try { + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 1); + fail("clientEncryptionPolicy should fail as id which is part of the partition key cannot be encrypted with " + + "PolicyFormatVersion 1."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Path /id cannot be encrypted with policyFormatVersion 1."); + } + } + + @Test(groups = {"emulator"}, timeOut = TIMEOUT) + public void createContainer_withPartitionKeyWrongEncryptionType() { String collectionName = UUID.randomUUID().toString(); CosmosContainerProperties containerProperties = getCollectionDefinition(collectionName); @@ -157,18 +268,17 @@ public void createContainer_withPartitionKeyInEncryption() { paths.add(path1); paths.add(path2); - ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths); + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 2); CosmosContainerResponse containerResponse = null; //Verify partition key in CosmosContainerProperties constructor with encrypted field. try { containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); containerResponse = createdDatabase.createContainer(containerProperties); - fail("createContainer should fail as mypk which is part of the partition key cannot be included in the " + - "ClientEncryptionPolicy."); + fail("createContainer should fail as mypk which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); } catch (IllegalArgumentException ex) { - assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be included in" + - " the ClientEncryptionPolicy."); + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key has to be encrypted with Deterministic type Encryption."); } @@ -178,11 +288,10 @@ public void createContainer_withPartitionKeyInEncryption() { try { containerProperties.setClientEncryptionPolicy(clientEncryptionPolicy); containerResponse = createdDatabase.createContainer(containerProperties); - fail("createContainer should fail as mypk which is part of the partition key cannot be included in the " + - "ClientEncryptionPolicy."); + fail("createContainer should fail as mypk which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); } catch (IllegalArgumentException ex) { - assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be included in" + - " the ClientEncryptionPolicy."); + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key has to be encrypted with Deterministic type Encryption."); } @@ -197,11 +306,10 @@ public void createContainer_withPartitionKeyInEncryption() { partitionKeyDefinition.setPaths(keyPaths); containerProperties.setPartitionKeyDefinition(partitionKeyDefinition); containerResponse = createdDatabase.createContainer(containerProperties); - fail("createContainer should fail as mypk which is part of the partition key cannot be included in the " + - "ClientEncryptionPolicy."); + fail("createContainer should fail as mypk which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); } catch (IllegalArgumentException ex) { - assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key cannot be included in" + - " the ClientEncryptionPolicy."); + assertThat(ex.getMessage()).isEqualTo("Path mypk which is part of the partition key has to be encrypted with Deterministic type Encryption."); } //This should pass as we check only the first key of the composite key. @@ -213,6 +321,36 @@ public void createContainer_withPartitionKeyInEncryption() { validateContainerResponseWithEncryption(containerProperties, containerResponse, clientEncryptionPolicy); } + @Test(groups = {"emulator"}, timeOut = TIMEOUT) + public void createEncryptionPolicy_withIdWrongEncryptionType() { + String collectionName = UUID.randomUUID().toString(); + CosmosContainerProperties containerProperties = getCollectionDefinition(collectionName); + + ClientEncryptionIncludedPath path1 = new ClientEncryptionIncludedPath(); + path1.setPath("/id"); + path1.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path1.setEncryptionType("Randomized"); + path1.setClientEncryptionKeyId("containerTestKey1"); + + ClientEncryptionIncludedPath path2 = new ClientEncryptionIncludedPath(); + path2.setPath("/path2"); + path2.setEncryptionAlgorithm("AEAD_AES_256_CBC_HMAC_SHA256"); + path2.setEncryptionType("Deterministic"); + path2.setClientEncryptionKeyId("containerTestKey2"); + + List paths = new ArrayList<>(); + paths.add(path1); + paths.add(path2); + + try { + ClientEncryptionPolicy clientEncryptionPolicy = new ClientEncryptionPolicy(paths, 2); + fail("clientEncryptionPolicy should fail as id which is part of the partition key has to be encrypted with " + + "Deterministic type Encryption."); + } catch (IllegalArgumentException ex) { + assertThat(ex.getMessage()).isEqualTo("Only deterministic encryption type is supported for path /id."); + } + } + @DataProvider public static Object[][] analyticalTTLProvider() { return new Object[][]{