From 26397c4dbd4f0686696784025276afbd3c58ece4 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Sat, 11 Feb 2023 06:59:23 +0100 Subject: [PATCH 01/24] Prepare issue branch. --- pom.xml | 2 +- spring-data-mongodb-benchmarks/pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index d864b2e4e6..4baadbafe8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-4284-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 1b2a1390e6..a98f47aa21 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-4284-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 8db8d798fb..8a28b2af05 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-4284-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 9a57f7eb52..2a0b07546d 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.1.0-SNAPSHOT + 4.1.x-GH-4284-SNAPSHOT ../pom.xml From 01428d5cbe24b398a5679179b6c596000cd2dc47 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 11 Nov 2022 09:09:05 +0100 Subject: [PATCH 02/24] Hacking --- spring-data-mongodb/pom.xml | 7 + .../data/mongodb/fle/FLETests.java | 284 ++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 2a0b07546d..d223076814 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -112,6 +112,13 @@ true + + org.mongodb + mongodb-crypt + 1.6.1 + true + + io.projectreactor reactor-core diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java new file mode 100644 index 0000000000..e305e14a04 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -0,0 +1,284 @@ +/* + * Copyright 2022 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.fle; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; + +import lombok.Data; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.bson.BsonBinary; +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.types.Binary; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.convert.PropertyValueConverterFactory; +import org.springframework.data.convert.ValueConverter; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; +import org.springframework.data.mongodb.core.convert.MongoValueConverter; +import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.fle.FLETests.Config; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.util.Lazy; +import org.springframework.lang.Nullable; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; + +/** + * @author Christoph Strobl + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = Config.class) +public class FLETests { + + @Autowired MongoTemplate template; + + @Test + void manualEnAndDecryption() { + + Person person = new Person(); + person.id = "id-1"; + person.name = "p1-name"; + person.ssn = "mySecretSSN"; // determinisitc encryption (queryable) + person.wallet = "myEvenMoreSecretStuff"; // random encryption (non queryable) + + template.save(person); + + System.out.println("source: " + person); + + Document savedDocument = template.execute(Person.class, collection -> { + return collection.find(new Document()).first(); + }); + + // ssn should look like "ssn": {"$binary": {"base64": "... + System.out.println("saved: " + savedDocument.toJson()); + assertThat(savedDocument.get("ssn")).isInstanceOf(org.bson.types.Binary.class); + assertThat(savedDocument.get("wallet")).isInstanceOf(org.bson.types.Binary.class); + + // count should be 1 using a deterministic algorithm + long queryCount = template.query(Person.class).matching(where("ssn").is(person.ssn)).count(); + System.out.println("query(count): " + queryCount); + assertThat(queryCount).isOne(); + + Person bySsn = template.query(Person.class).matching(where("ssn").is(person.ssn)).firstValue(); + System.out.println("queryable: " + bySsn); + assertThat(bySsn).isEqualTo(person); + + Person byWallet = template.query(Person.class).matching(where("wallet").is(person.wallet)).firstValue(); + System.out.println("not-queryable: " + byWallet); + assertThat(byWallet).isNull(); + } + + @Configuration + static class Config extends AbstractMongoClientConfiguration { + + @Autowired ApplicationContext applicationContext; + + @Override + protected String getDatabaseName() { + return "fle-test"; + } + + @Bean + public MongoClient mongoClient() { + return super.mongoClient(); + } + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) { + + converterConfigurationAdapter + .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext)); + } + + @Bean + EncryptingConverter encryptingConverter(ClientEncryption clientEncryption) { + return new EncryptingConverter(clientEncryption); + } + + @Bean + ClientEncryption clientEncryption(MongoClient mongoClient) { + + final byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + Map> kmsProviders = new HashMap>() { + { + put("local", new HashMap() { + { + put("key", localMasterKey); + } + }); + } + }; + + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames"))); + + MongoCollection collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test"); + collection.drop(); // Clear old data + + // Create the ClientEncryption instance + ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings( + MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) + .keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build(); + ClientEncryption clientEncryption = ClientEncryptions.create(clientEncryptionSettings); + return clientEncryption; + } + } + + @Data + @org.springframework.data.mongodb.core.mapping.Document("test") + static class Person { + + String id; + String name; + + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + String ssn; + + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // + String wallet; + } + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.FIELD) + @Encrypted + @ValueConverter(EncryptingConverter.class) + @interface EncryptedField { + + @AliasFor(annotation = Encrypted.class, value = "algorithm") + String algorithm() default ""; + + String altKeyName() default ""; + } + + static class EncryptingConverter implements MongoValueConverter { + + private ClientEncryption clientEncryption; + private BsonBinary dataKeyId; // should be provided from outside. + + public EncryptingConverter(ClientEncryption clientEncryption) { + + this.clientEncryption = clientEncryption; + this.dataKeyId = clientEncryption.createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))); + } + + @Nullable + @Override + public Object read(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + Object decrypted = encryptionContext.decrypt(value, clientEncryption); + return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; + } + + @Nullable + @Override + public BsonBinary write(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + return encryptionContext.encrypt(value, clientEncryption); + } + + ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { + return new ManualEncryptionContext(context.getProperty(), this.dataKeyId); + } + } + + static class ManualEncryptionContext { + + MongoPersistentProperty persistentProperty; + BsonBinary dataKeyId; + Lazy encryption; + + public ManualEncryptionContext(MongoPersistentProperty persistentProperty, BsonBinary dataKeyId) { + this.persistentProperty = persistentProperty; + this.dataKeyId = dataKeyId; + this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); + } + + BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { + + // TODO: check - encryption.get().keyId() + + EncryptOptions encryptOptions = new EncryptOptions(encryption.get().algorithm()); + + EncryptedField annotation = persistentProperty.findAnnotation(EncryptedField.class); + if (annotation != null && !annotation.altKeyName().isBlank()) { + encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); + } else { + encryptOptions = encryptOptions.keyId(this.dataKeyId); + } + + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + } + + public Object decrypt(Object value, ClientEncryption clientEncryption) { + + if (value instanceof Binary binary) { + return clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); + } + if (value instanceof BsonBinary binary) { + return clientEncryption.decrypt(binary); + } + + // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java + return value; + } + } +} From 8295ef75055ee7b690d707aff5e7859c894ea4e7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 14 Nov 2022 08:34:25 +0100 Subject: [PATCH 03/24] Enable full encryption of nested documents. --- .../data/mongodb/fle/FLETests.java | 78 +++++++++++++++++-- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index e305e14a04..00a9c11e3f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -30,7 +30,11 @@ import java.util.HashMap; import java.util.Map; +import lombok.Generated; +import lombok.Getter; +import lombok.Setter; import org.bson.BsonBinary; +import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.Document; import org.bson.types.Binary; @@ -89,6 +93,16 @@ void manualEnAndDecryption() { person.ssn = "mySecretSSN"; // determinisitc encryption (queryable) person.wallet = "myEvenMoreSecretStuff"; // random encryption (non queryable) + // nested full document encryption + person.address = new Address(); + person.address.city = "NYC"; + person.address.street = "4th Ave."; + + person.encryptedZip = new AddressWithEncryptedZip(); + person.encryptedZip.city = "Boston"; + person.encryptedZip.street = "central square"; + person.encryptedZip.zip = "1234567890"; + template.save(person); System.out.println("source: " + person); @@ -191,6 +205,33 @@ static class Person { @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // String wallet; + + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + Address address; + + AddressWithEncryptedZip encryptedZip; + } + + @Data + static class Address { + String city; + String street; + } + + @Getter @Setter + static class AddressWithEncryptedZip extends Address { + + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) + String zip; + + @Override + public String toString() { + return "AddressWithEncryptedZip{" + + "zip='" + zip + '\'' + + ", city='" + getCity() + '\'' + + ", street='" + getStreet() + '\'' + + '}'; + } } @Retention(RetentionPolicy.RUNTIME) @@ -235,18 +276,20 @@ public BsonBinary write(Object value, MongoConversionContext context) { } ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { - return new ManualEncryptionContext(context.getProperty(), this.dataKeyId); + return new ManualEncryptionContext(context, this.dataKeyId); } } static class ManualEncryptionContext { + MongoConversionContext context; MongoPersistentProperty persistentProperty; BsonBinary dataKeyId; Lazy encryption; - public ManualEncryptionContext(MongoPersistentProperty persistentProperty, BsonBinary dataKeyId) { - this.persistentProperty = persistentProperty; + public ManualEncryptionContext(MongoConversionContext context, BsonBinary dataKeyId) { + this.context = context; + this.persistentProperty = context.getProperty(); this.dataKeyId = dataKeyId; this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); } @@ -264,21 +307,42 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { encryptOptions = encryptOptions.keyId(this.dataKeyId); } - return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + if (!persistentProperty.isEntity()) { + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + } + + Object write = context.write(value); + if (write instanceof Document doc) { + return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); + } + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); } public Object decrypt(Object value, ClientEncryption clientEncryption) { + Object result = value; if (value instanceof Binary binary) { - return clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); + result = clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); } if (value instanceof BsonBinary binary) { - return clientEncryption.decrypt(binary); + result = clientEncryption.decrypt(binary); } // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java - return value; + if (value == result) { + return result; + } + + if (!persistentProperty.isEntity() && result instanceof BsonValue bsonValue) { + return BsonUtils.toJavaType(bsonValue); + } + + if (persistentProperty.isEntity() && result instanceof BsonDocument bsonDocument) { + return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); + } + + return result; } } } From 8c64194c85af258f0ee18778353c17e232dd29e6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 14 Nov 2022 11:57:00 +0100 Subject: [PATCH 04/24] Encrypt collection of simple values --- .../data/mongodb/fle/FLETests.java | 73 ++++++++++++++++--- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index 00a9c11e3f..ec42cf640c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -20,19 +20,22 @@ import static org.springframework.data.mongodb.core.query.Criteria.*; import lombok.Data; +import lombok.Getter; +import lombok.Setter; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; -import lombok.Generated; -import lombok.Getter; -import lombok.Setter; +import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonDocument; import org.bson.BsonValue; @@ -44,6 +47,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.core.CollectionFactory; import org.springframework.core.annotation.AliasFor; import org.springframework.data.convert.PropertyValueConverterFactory; import org.springframework.data.convert.ValueConverter; @@ -60,6 +64,7 @@ import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.ObjectUtils; import com.mongodb.ClientEncryptionSettings; import com.mongodb.ConnectionString; @@ -103,6 +108,8 @@ void manualEnAndDecryption() { person.encryptedZip.street = "central square"; person.encryptedZip.zip = "1234567890"; + person.listOfString = Arrays.asList("spring", "data", "mongodb"); + template.save(person); System.out.println("source: " + person); @@ -206,10 +213,14 @@ static class Person { @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // String wallet; - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random Address address; AddressWithEncryptedZip encryptedZip; + + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + List listOfString; + } @Data @@ -218,19 +229,16 @@ static class Address { String street; } - @Getter @Setter + @Getter + @Setter static class AddressWithEncryptedZip extends Address { - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) - String zip; + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip; @Override public String toString() { - return "AddressWithEncryptedZip{" + - "zip='" + zip + '\'' + - ", city='" + getCity() + '\'' + - ", street='" + getStreet() + '\'' + - '}'; + return "AddressWithEncryptedZip{" + "zip='" + zip + '\'' + ", city='" + getCity() + '\'' + ", street='" + + getStreet() + '\'' + '}'; } } @@ -308,6 +316,10 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { } if (!persistentProperty.isEntity()) { + + if (persistentProperty.isCollectionLike()) { + return clientEncryption.encrypt(collectionLikeToBsonValue(value), encryptOptions); + } return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); } @@ -318,6 +330,35 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); } + public BsonValue collectionLikeToBsonValue(Object value) { + + if (persistentProperty.isCollectionLike()) { + + BsonArray bsonArray = new BsonArray(); + if (!persistentProperty.isEntity()) { + if (value instanceof Collection values) { + values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + bsonArray.add(BsonUtils.simpleToBsonValue(o)); + } + } + return bsonArray; + } + } + + if (!persistentProperty.isEntity()) { + if (persistentProperty.isCollectionLike()) { + + if (persistentProperty.isEntity()) { + + } + } + } + + return null; + } + public Object decrypt(Object value, ClientEncryption clientEncryption) { Object result = value; @@ -334,6 +375,14 @@ public Object decrypt(Object value, ClientEncryption clientEncryption) { return result; } + if (persistentProperty.isCollectionLike() && result instanceof Iterable iterable) { + if (!persistentProperty.isEntity()) { + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); + return collection; + } + } + if (!persistentProperty.isEntity() && result instanceof BsonValue bsonValue) { return BsonUtils.toJavaType(bsonValue); } From e571bc2d0fe7c73de65fb288cad3d43bf371c6d7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 14 Nov 2022 12:45:44 +0100 Subject: [PATCH 05/24] Encrypt collection of complex types. --- .../data/mongodb/fle/FLETests.java | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index ec42cf640c..b8f15f42db 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -110,6 +110,11 @@ void manualEnAndDecryption() { person.listOfString = Arrays.asList("spring", "data", "mongodb"); + Address partOfList = new Address(); + partOfList.city = "SFO"; + partOfList.street = "---"; + person.listOfComplex = Collections.singletonList(partOfList); + template.save(person); System.out.println("source: " + person); @@ -120,8 +125,13 @@ void manualEnAndDecryption() { // ssn should look like "ssn": {"$binary": {"base64": "... System.out.println("saved: " + savedDocument.toJson()); - assertThat(savedDocument.get("ssn")).isInstanceOf(org.bson.types.Binary.class); - assertThat(savedDocument.get("wallet")).isInstanceOf(org.bson.types.Binary.class); + assertThat(savedDocument.get("ssn")).isInstanceOf(Binary.class); + assertThat(savedDocument.get("wallet")).isInstanceOf(Binary.class); + assertThat(savedDocument.get("encryptedZip")).isInstanceOf(Document.class); + assertThat(savedDocument.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class); + assertThat(savedDocument.get("address")).isInstanceOf(Binary.class); + assertThat(savedDocument.get("listOfString")).isInstanceOf(Binary.class); + assertThat(savedDocument.get("listOfComplex")).isInstanceOf(Binary.class); // count should be 1 using a deterministic algorithm long queryCount = template.query(Person.class).matching(where("ssn").is(person.ssn)).count(); @@ -221,6 +231,9 @@ static class Person { @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random List listOfString; + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + List
listOfComplex; + } @Data @@ -322,6 +335,9 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { } return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); } + if (persistentProperty.isCollectionLike()) { + return clientEncryption.encrypt(collectionLikeToBsonValue(value), encryptOptions); + } Object write = context.write(value); if (write instanceof Document doc) { @@ -344,6 +360,19 @@ public BsonValue collectionLikeToBsonValue(Object value) { } } return bsonArray; + } else { + if (value instanceof Collection values) { + values.forEach(it -> { + Document write = (Document) context.write(it, persistentProperty.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); + }); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + Document write = (Document) context.write(0, persistentProperty.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); + } + } + return bsonArray; } } @@ -380,6 +409,12 @@ public Object decrypt(Object value, ClientEncryption clientEncryption) { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); return collection; + } else { + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + iterable.forEach(it -> { + collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); + }); + return collection; } } From 52905befb890be1621837a7752eda36313b0b997 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 15 Nov 2022 14:27:05 +0100 Subject: [PATCH 06/24] Some changes that allow reading the alt key from a field typically only supported in automatic schema but neat to have it here as well. eg. for customer data cyper based on eg. username. Also make sure to translate decryption exceptions. --- .../core/MongoExceptionTranslator.java | 4 + .../core/convert/MappingMongoConverter.java | 28 +++++-- .../core/convert/MongoConversionContext.java | 10 ++- .../mongodb/core/convert/QueryMapper.java | 2 +- .../MappingMongoConverterUnitTests.java | 2 +- .../data/mongodb/fle/FLETests.java | 73 ++++++++++++++++++- 6 files changed, 107 insertions(+), 12 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java index 08fd0c36dd..f3b4e6a718 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java @@ -68,6 +68,8 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator private static final Set DATA_INTEGRITY_EXCEPTIONS = new HashSet<>( Arrays.asList("WriteConcernException", "MongoWriteException", "MongoBulkWriteException")); + private static final Set SECURITY_EXCEPTIONS = Set.of("MongoCryptException"); + @Nullable public DataAccessException translateExceptionIfPossible(RuntimeException ex) { @@ -131,6 +133,8 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return new ClientSessionException(ex.getMessage(), ex); } else if (MongoDbErrorCodes.isTransactionFailureCode(code)) { return new MongoTransactionException(ex.getMessage(), ex); + } else if(ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) { + return new PermissionDeniedDataAccessException(ex.getMessage(), ex); } return new UncategorizedMongoDbException(ex.getMessage(), ex); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 43d7d39a82..9865c31d69 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -868,9 +868,9 @@ private void writeProperties(Bson bson, MongoPersistentEntity entity, Persist dbObjectAccessor.put(prop, null); } } else if (!conversions.isSimpleType(value.getClass())) { - writePropertyInternal(value, dbObjectAccessor, prop); + writePropertyInternal(value, dbObjectAccessor, prop, accessor); } else { - writeSimpleInternal(value, bson, prop); + writeSimpleInternal(value, bson, prop, accessor); } } } @@ -887,11 +887,11 @@ private void writeAssociation(Association association, return; } - writePropertyInternal(value, dbObjectAccessor, inverseProp); + writePropertyInternal(value, dbObjectAccessor, inverseProp, accessor); } @SuppressWarnings({ "unchecked" }) - protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor accessor, MongoPersistentProperty prop) { + protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor accessor, MongoPersistentProperty prop, PersistentPropertyAccessor persistentPropertyAccessor) { if (obj == null) { return; @@ -902,7 +902,13 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce if (conversions.hasValueConverter(prop)) { accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj, - new MongoConversionContext(prop, this))); + new MongoConversionContext(new PropertyValueProvider() { + @Nullable + @Override + public T getPropertyValue(MongoPersistentProperty property) { + return (T) persistentPropertyAccessor.getProperty(property); + } + }, prop, this))); return; } @@ -1234,12 +1240,18 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, String key) BsonUtils.addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class)); } - private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property) { + private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property, PersistentPropertyAccessor persistentPropertyAccessor) { DocumentAccessor accessor = new DocumentAccessor(bson); if (conversions.hasValueConverter(property)) { accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value, - new MongoConversionContext(property, this))); + new MongoConversionContext(new PropertyValueProvider() { + @Nullable + @Override + public T getPropertyValue(MongoPersistentProperty property) { + return (T) persistentPropertyAccessor.getProperty(property); + } + }, property, this))); return; } @@ -1892,7 +1904,7 @@ public T getPropertyValue(MongoPersistentProperty property) { CustomConversions conversions = context.getCustomConversions(); if (conversions.hasValueConverter(property)) { return (T) conversions.getPropertyValueConversions().getValueConverter(property).read(value, - new MongoConversionContext(property, context.getSourceConverter())); + new MongoConversionContext(this, property, context.getSourceConverter())); } ConversionContext contextToUse = context.forProperty(property); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index f4a1ea7bd1..e3a67fbee8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -17,6 +17,8 @@ import org.bson.conversions.Bson; import org.springframework.data.convert.ValueConversionContext; +import org.springframework.data.mapping.PersistentPropertyAccessor; +import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; @@ -29,11 +31,13 @@ */ public class MongoConversionContext implements ValueConversionContext { + private final PropertyValueProvider accessor; // TODO: generics private final MongoPersistentProperty persistentProperty; private final MongoConverter mongoConverter; - public MongoConversionContext(MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { + public MongoConversionContext(PropertyValueProvider accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { + this.accessor = accessor; this.persistentProperty = persistentProperty; this.mongoConverter = mongoConverter; } @@ -43,6 +47,10 @@ public MongoPersistentProperty getProperty() { return persistentProperty; } + public Object getValue(String propertyPath) { + return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath)); + } + @Override public T write(@Nullable Object value, TypeInformation target) { return (T) mongoConverter.convertToMongoType(value, target); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 9302d37cae..8e0908d780 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -437,7 +437,7 @@ protected Object getMappedValue(Field documentField, Object sourceValue) { && converter.getCustomConversions().hasValueConverter(documentField.getProperty())) { return converter.getCustomConversions().getPropertyValueConversions() .getValueConverter(documentField.getProperty()) - .write(value, new MongoConversionContext(documentField.getProperty(), converter)); + .write(value, new MongoConversionContext(null, documentField.getProperty(), converter)); } if (documentField.isIdField() && !documentField.isAssociation()) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index ad6b5135ce..e211f2ce3b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -2613,7 +2613,7 @@ void readsMapThatDoesNotComeAsDocument() { doReturn(Person.class).when(persistentProperty).getType(); doReturn(Person.class).when(persistentProperty).getRawType(); - converter.writePropertyInternal(sourceValue, accessor, persistentProperty); + converter.writePropertyInternal(sourceValue, accessor, persistentProperty, null); assertThat(accessor.getDocument()) .isEqualTo(new org.bson.Document("pName", new org.bson.Document("_id", id.toString()))); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index b8f15f42db..7595d3e14f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -34,6 +34,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; import org.bson.BsonArray; import org.bson.BsonBinary; @@ -49,6 +50,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.CollectionFactory; import org.springframework.core.annotation.AliasFor; +import org.springframework.dao.PermissionDeniedDataAccessException; import org.springframework.data.convert.PropertyValueConverterFactory; import org.springframework.data.convert.ValueConverter; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; @@ -65,6 +67,7 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; import com.mongodb.ClientEncryptionSettings; import com.mongodb.ConnectionString; @@ -77,6 +80,7 @@ import com.mongodb.client.model.Indexes; import com.mongodb.client.model.vault.DataKeyOptions; import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.result.DeleteResult; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryptions; @@ -147,6 +151,56 @@ void manualEnAndDecryption() { assertThat(byWallet).isNull(); } + @Test + void altKeyDetection(@Autowired ClientEncryption clientEncryption) throws InterruptedException { + + BsonBinary user1key = clientEncryption.createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("user-1"))); + + BsonBinary user2key = clientEncryption.createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("user-2"))); + + Person p1 = new Person(); + p1.id = "id-1"; + p1.name = "user-1"; + p1.ssn = "ssn"; + p1.viaAltKeyNameField = "value-1"; + + Person p2 = new Person(); + p2.id = "id-2"; + p2.name = "user-2"; + p2.viaAltKeyNameField = "value-1"; + + Person p3 = new Person(); + p3.id = "id-3"; + p3.name = "user-1"; + p3.viaAltKeyNameField = "value-1"; + + template.save(p1); + template.save(p2); + template.save(p3); + + template.execute(Person.class, collection -> { + collection.find(new Document()).forEach(it -> System.out.println(it.toJson())); + return null; + }); + + // System.out.println(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()); + // System.out.println(template.query(Person.class).matching(where("id").is(p2.id)).firstValue()); + + DeleteResult deleteResult = clientEncryption.deleteKey(user2key); + clientEncryption.getKeys().forEach(System.out::println); + System.out.println("deleteResult: " + deleteResult); + + System.out.println("---- waiting for cache timeout ----"); + TimeUnit.SECONDS.sleep(90); + + assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1); + + assertThatExceptionOfType(PermissionDeniedDataAccessException.class) + .isThrownBy(() -> template.query(Person.class).matching(where("id").is(p2.id)).firstValue()); + } + @Configuration static class Config extends AbstractMongoClientConfiguration { @@ -234,6 +288,8 @@ static class Person { @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random List
listOfComplex; + @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") // + String viaAltKeyNameField; } @Data @@ -323,11 +379,21 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { EncryptedField annotation = persistentProperty.findAnnotation(EncryptedField.class); if (annotation != null && !annotation.altKeyName().isBlank()) { - encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); + if (annotation.altKeyName().startsWith("/")) { + String fieldName = annotation.altKeyName().replace("/", ""); + Object altKeyNameValue = context.getValue(fieldName); + encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); + } else { + encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); + } } else { encryptOptions = encryptOptions.keyId(this.dataKeyId); } + System.out.println( + "encrypting with: " + (StringUtils.hasText(encryptOptions.getKeyAltName()) ? encryptOptions.getKeyAltName() + : encryptOptions.getKeyId())); + if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { @@ -390,6 +456,11 @@ public BsonValue collectionLikeToBsonValue(Object value) { public Object decrypt(Object value, ClientEncryption clientEncryption) { + // this was a hack to avoid the 60 sec timeout of the key cache + // ClientEncryptionSettings settings = (ClientEncryptionSettings) new DirectFieldAccessor(clientEncryption) + // .getPropertyValue("options"); + // clientEncryption = ClientEncryptions.create(settings); + Object result = value; if (value instanceof Binary binary) { result = clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); From 0dae0ff19037fbcc2566b382b242cb49250cc5fc Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 16 Nov 2022 10:12:33 +0100 Subject: [PATCH 07/24] Test encryption during update --- .../data/mongodb/fle/FLETests.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index 7595d3e14f..8d51b35624 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -60,6 +60,7 @@ import org.springframework.data.mongodb.core.convert.MongoValueConverter; import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.fle.FLETests.Config; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.util.Lazy; @@ -151,6 +152,30 @@ void manualEnAndDecryption() { assertThat(byWallet).isNull(); } + @Test + void theUpdateStuff() { + + Person person = new Person(); + person.id = "id-1"; + person.name = "p1-name"; + + template.save(person); + + Document savedDocument = template.execute(Person.class, collection -> { + return collection.find(new Document()).first(); + }); + System.out.println("saved: " + savedDocument.toJson()); + + template.update(Person.class).matching(where("id").is(person.id)).apply(Update.update("ssn", "secret-value")).first(); + + savedDocument = template.execute(Person.class, collection -> { + return collection.find(new Document()).first(); + }); + System.out.println("updated: " + savedDocument.toJson()); + assertThat(savedDocument.get("ssn")).isInstanceOf(Binary.class); + + } + @Test void altKeyDetection(@Autowired ClientEncryption clientEncryption) throws InterruptedException { From a89745a3057bf5e6111cc6af82db9a9054481a64 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Sat, 11 Feb 2023 07:22:24 +0100 Subject: [PATCH 08/24] move inner classes to dedicated package --- .../core/encryption/EncryptingConverter.java | 81 +++++++ .../core/encryption/ExplicitlyEncrypted.java | 62 +++++ .../encryption/ManualEncryptionContext.java | 199 +++++++++++++++ .../data/mongodb/fle/FLETests.java | 229 +----------------- 4 files changed, 353 insertions(+), 218 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java new file mode 100644 index 0000000000..bc58e11d1a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java @@ -0,0 +1,81 @@ +/* + * Copyright 2023. the original author or authors. + * + * 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. + */ + +/* + * Copyright 2023 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.encryption; + +import java.util.Collections; + +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.vault.ClientEncryption; +import org.bson.BsonBinary; +import org.bson.BsonValue; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoValueConverter; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2023/02 + */ +public class EncryptingConverter implements MongoValueConverter { + + private ClientEncryption clientEncryption; + private BsonBinary dataKeyId; // should be provided from outside. + + public EncryptingConverter(ClientEncryption clientEncryption) { + + this.clientEncryption = clientEncryption; + this.dataKeyId = clientEncryption.createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))); + } + + @Nullable + @Override + public Object read(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + Object decrypted = encryptionContext.decrypt(value, clientEncryption); + return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; + } + + @Nullable + @Override + public BsonBinary write(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + return encryptionContext.encrypt(value, clientEncryption); + } + + ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { + return new ManualEncryptionContext(context, this.dataKeyId); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java new file mode 100644 index 0000000000..90fe3b528b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java @@ -0,0 +1,62 @@ +/* + * Copyright 2023. the original author or authors. + * + * 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. + */ + +/* + * Copyright 2023 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.encryption; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.convert.PropertyValueConverter; +import org.springframework.data.convert.PropertyValueConverter.ObjectToObjectPropertyValueConverter; +import org.springframework.data.convert.ValueConverter; +import org.springframework.data.mongodb.core.mapping.Encrypted; + +/** + * @author Christoph Strobl + * @since 2023/02 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Encrypted +@ValueConverter +public @interface ExplicitlyEncrypted { + + @AliasFor(annotation = Encrypted.class, value = "algorithm") + String algorithm() default ""; + + String altKeyName() default ""; + + @AliasFor(annotation = ValueConverter.class, value = "value") + Class value() default EncryptingConverter.class; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java new file mode 100644 index 0000000000..47487edf52 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java @@ -0,0 +1,199 @@ +/* + * Copyright 2023. the original author or authors. + * + * 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. + */ + +/* + * Copyright 2023 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.encryption; + +import java.util.Collection; + +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.types.Binary; +import org.springframework.core.CollectionFactory; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.util.Lazy; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2023/02 + */ +public class ManualEncryptionContext { + + MongoConversionContext context; + MongoPersistentProperty persistentProperty; + BsonBinary dataKeyId; + Lazy encryption; + + public ManualEncryptionContext(MongoConversionContext context, BsonBinary dataKeyId) { + this.context = context; + this.persistentProperty = context.getProperty(); + this.dataKeyId = dataKeyId; + this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); + } + + BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { + + // TODO: check - encryption.get().keyId() + + EncryptOptions encryptOptions = new EncryptOptions(encryption.get().algorithm()); + + ExplicitlyEncrypted annotation = persistentProperty.findAnnotation(ExplicitlyEncrypted.class); + if (annotation != null && !annotation.altKeyName().isBlank()) { + if (annotation.altKeyName().startsWith("/")) { + String fieldName = annotation.altKeyName().replace("/", ""); + Object altKeyNameValue = context.getValue(fieldName); + encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); + } else { + encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); + } + } else { + encryptOptions = encryptOptions.keyId(this.dataKeyId); + } + + System.out.println( + "encrypting with: " + (StringUtils.hasText(encryptOptions.getKeyAltName()) ? encryptOptions.getKeyAltName() + : encryptOptions.getKeyId())); + + if (!persistentProperty.isEntity()) { + + if (persistentProperty.isCollectionLike()) { + return clientEncryption.encrypt(collectionLikeToBsonValue(value), encryptOptions); + } + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + } + if (persistentProperty.isCollectionLike()) { + return clientEncryption.encrypt(collectionLikeToBsonValue(value), encryptOptions); + } + + Object write = context.write(value); + if (write instanceof Document doc) { + return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); + } + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); + } + + public BsonValue collectionLikeToBsonValue(Object value) { + + if (persistentProperty.isCollectionLike()) { + + BsonArray bsonArray = new BsonArray(); + if (!persistentProperty.isEntity()) { + if (value instanceof Collection values) { + values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + bsonArray.add(BsonUtils.simpleToBsonValue(o)); + } + } + return bsonArray; + } else { + if (value instanceof Collection values) { + values.forEach(it -> { + Document write = (Document) context.write(it, persistentProperty.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); + }); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + Document write = (Document) context.write(0, persistentProperty.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); + } + } + return bsonArray; + } + } + + if (!persistentProperty.isEntity()) { + if (persistentProperty.isCollectionLike()) { + + if (persistentProperty.isEntity()) { + + } + } + } + + return null; + } + + public Object decrypt(Object value, ClientEncryption clientEncryption) { + + // this was a hack to avoid the 60 sec timeout of the key cache + // ClientEncryptionSettings settings = (ClientEncryptionSettings) new DirectFieldAccessor(clientEncryption) + // .getPropertyValue("options"); + // clientEncryption = ClientEncryptions.create(settings); + + Object result = value; + if (value instanceof Binary binary) { + result = clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); + } + if (value instanceof BsonBinary binary) { + result = clientEncryption.decrypt(binary); + } + + // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java + if (value == result) { + return result; + } + + if (persistentProperty.isCollectionLike() && result instanceof Iterable iterable) { + if (!persistentProperty.isEntity()) { + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); + return collection; + } else { + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + iterable.forEach(it -> { + collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); + }); + return collection; + } + } + + if (!persistentProperty.isEntity() && result instanceof BsonValue bsonValue) { + return BsonUtils.toJavaType(bsonValue); + } + + if (persistentProperty.isEntity() && result instanceof BsonDocument bsonDocument) { + return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); + } + + return result; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index 8d51b35624..0182cd8dba 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -23,52 +23,35 @@ import lombok.Getter; import lombok.Setter; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; import java.security.SecureRandom; import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; -import org.bson.BsonArray; import org.bson.BsonBinary; -import org.bson.BsonDocument; -import org.bson.BsonValue; import org.bson.Document; import org.bson.types.Binary; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.CollectionFactory; -import org.springframework.core.annotation.AliasFor; import org.springframework.dao.PermissionDeniedDataAccessException; import org.springframework.data.convert.PropertyValueConverterFactory; -import org.springframework.data.convert.ValueConverter; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; -import org.springframework.data.mongodb.core.convert.MongoValueConverter; -import org.springframework.data.mongodb.core.mapping.Encrypted; -import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.encryption.ExplicitlyEncrypted; +import org.springframework.data.mongodb.core.encryption.EncryptingConverter; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.fle.FLETests.Config; -import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import com.mongodb.ClientEncryptionSettings; import com.mongodb.ConnectionString; @@ -80,7 +63,6 @@ import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.Indexes; import com.mongodb.client.model.vault.DataKeyOptions; -import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryptions; @@ -177,6 +159,7 @@ void theUpdateStuff() { } @Test + @Disabled("for now - takes to long ") void altKeyDetection(@Autowired ClientEncryption clientEncryption) throws InterruptedException { BsonBinary user1key = clientEncryption.createDataKey("local", @@ -296,24 +279,24 @@ static class Person { String id; String name; - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // String ssn; - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // String wallet; - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random Address address; AddressWithEncryptedZip encryptedZip; - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random List listOfString; - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random List
listOfComplex; - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") // + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") // String viaAltKeyNameField; } @@ -327,7 +310,7 @@ static class Address { @Setter static class AddressWithEncryptedZip extends Address { - @EncryptedField(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip; + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip; @Override public String toString() { @@ -335,194 +318,4 @@ public String toString() { + getStreet() + '\'' + '}'; } } - - @Retention(RetentionPolicy.RUNTIME) - @Target(ElementType.FIELD) - @Encrypted - @ValueConverter(EncryptingConverter.class) - @interface EncryptedField { - - @AliasFor(annotation = Encrypted.class, value = "algorithm") - String algorithm() default ""; - - String altKeyName() default ""; - } - - static class EncryptingConverter implements MongoValueConverter { - - private ClientEncryption clientEncryption; - private BsonBinary dataKeyId; // should be provided from outside. - - public EncryptingConverter(ClientEncryption clientEncryption) { - - this.clientEncryption = clientEncryption; - this.dataKeyId = clientEncryption.createDataKey("local", - new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))); - } - - @Nullable - @Override - public Object read(Object value, MongoConversionContext context) { - - ManualEncryptionContext encryptionContext = buildEncryptionContext(context); - Object decrypted = encryptionContext.decrypt(value, clientEncryption); - return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; - } - - @Nullable - @Override - public BsonBinary write(Object value, MongoConversionContext context) { - - ManualEncryptionContext encryptionContext = buildEncryptionContext(context); - return encryptionContext.encrypt(value, clientEncryption); - } - - ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { - return new ManualEncryptionContext(context, this.dataKeyId); - } - } - - static class ManualEncryptionContext { - - MongoConversionContext context; - MongoPersistentProperty persistentProperty; - BsonBinary dataKeyId; - Lazy encryption; - - public ManualEncryptionContext(MongoConversionContext context, BsonBinary dataKeyId) { - this.context = context; - this.persistentProperty = context.getProperty(); - this.dataKeyId = dataKeyId; - this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); - } - - BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { - - // TODO: check - encryption.get().keyId() - - EncryptOptions encryptOptions = new EncryptOptions(encryption.get().algorithm()); - - EncryptedField annotation = persistentProperty.findAnnotation(EncryptedField.class); - if (annotation != null && !annotation.altKeyName().isBlank()) { - if (annotation.altKeyName().startsWith("/")) { - String fieldName = annotation.altKeyName().replace("/", ""); - Object altKeyNameValue = context.getValue(fieldName); - encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); - } else { - encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); - } - } else { - encryptOptions = encryptOptions.keyId(this.dataKeyId); - } - - System.out.println( - "encrypting with: " + (StringUtils.hasText(encryptOptions.getKeyAltName()) ? encryptOptions.getKeyAltName() - : encryptOptions.getKeyId())); - - if (!persistentProperty.isEntity()) { - - if (persistentProperty.isCollectionLike()) { - return clientEncryption.encrypt(collectionLikeToBsonValue(value), encryptOptions); - } - return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); - } - if (persistentProperty.isCollectionLike()) { - return clientEncryption.encrypt(collectionLikeToBsonValue(value), encryptOptions); - } - - Object write = context.write(value); - if (write instanceof Document doc) { - return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); - } - return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); - } - - public BsonValue collectionLikeToBsonValue(Object value) { - - if (persistentProperty.isCollectionLike()) { - - BsonArray bsonArray = new BsonArray(); - if (!persistentProperty.isEntity()) { - if (value instanceof Collection values) { - values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - bsonArray.add(BsonUtils.simpleToBsonValue(o)); - } - } - return bsonArray; - } else { - if (value instanceof Collection values) { - values.forEach(it -> { - Document write = (Document) context.write(it, persistentProperty.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); - }); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) context.write(0, persistentProperty.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); - } - } - return bsonArray; - } - } - - if (!persistentProperty.isEntity()) { - if (persistentProperty.isCollectionLike()) { - - if (persistentProperty.isEntity()) { - - } - } - } - - return null; - } - - public Object decrypt(Object value, ClientEncryption clientEncryption) { - - // this was a hack to avoid the 60 sec timeout of the key cache - // ClientEncryptionSettings settings = (ClientEncryptionSettings) new DirectFieldAccessor(clientEncryption) - // .getPropertyValue("options"); - // clientEncryption = ClientEncryptions.create(settings); - - Object result = value; - if (value instanceof Binary binary) { - result = clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); - } - if (value instanceof BsonBinary binary) { - result = clientEncryption.decrypt(binary); - } - - // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active - // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java - if (value == result) { - return result; - } - - if (persistentProperty.isCollectionLike() && result instanceof Iterable iterable) { - if (!persistentProperty.isEntity()) { - Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); - iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); - return collection; - } else { - Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); - iterable.forEach(it -> { - collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); - }); - return collection; - } - } - - if (!persistentProperty.isEntity() && result instanceof BsonValue bsonValue) { - return BsonUtils.toJavaType(bsonValue); - } - - if (persistentProperty.isEntity() && result instanceof BsonDocument bsonDocument) { - return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); - } - - return result; - } - } } From a49e1c7b4a3855f0e488817cb23c7b251e2789ec Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Sat, 11 Feb 2023 09:18:27 +0100 Subject: [PATCH 09/24] Hacking --- .../encryption/ClientEncryptionConverter.java | 80 +++++++++++++++++++ .../core/encryption/EncryptingConverter.java | 61 +++----------- .../core/encryption/EncryptionContext.java | 46 +++++++++++ .../core/encryption/ExplicitlyEncrypted.java | 23 +----- .../core/encryption/KeyIdProvider.java | 25 ++++++ .../encryption/ManualEncryptionContext.java | 15 ++-- .../data/mongodb/fle/FLETests.java | 6 +- 7 files changed, 178 insertions(+), 78 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java new file mode 100644 index 0000000000..4db3831242 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import java.util.Collections; + +import org.bson.BsonBinary; +import org.bson.BsonValue; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.util.Lazy; +import org.springframework.lang.Nullable; + +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.vault.ClientEncryption; + +/** + * @author Christoph Strobl + */ +public class ClientEncryptionConverter implements EncryptingConverter { + + private ClientEncryption clientEncryption; + private final KeyIdProvider keyIdProvider; + + public ClientEncryptionConverter(ClientEncryption clientEncryption, KeyIdProvider keyIdProvider) { + + this.clientEncryption = clientEncryption; + if (keyIdProvider != null) { + this.keyIdProvider = keyIdProvider; + } else { + Lazy dataKey = Lazy.of(() -> clientEncryption.createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); + this.keyIdProvider = (ctx) -> dataKey.get(); + } + } + + @Nullable + @Override + public Object read(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + Object decrypted = encryptionContext.decrypt(value, clientEncryption); + return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; + } + + @Nullable + @Override + public BsonBinary write(Object value, MongoConversionContext context) { + + ManualEncryptionContext encryptionContext = buildEncryptionContext(context); + return encryptionContext.encrypt(value, clientEncryption); + } + + @Override + public Object decrypt(Object value, EncryptionContext context) { + return null; + } + + @Override + public Object encrypt(Object value, EncryptionContext context) { + return null; + } + + public ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { + return new ManualEncryptionContext(context, this.keyIdProvider); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java index bc58e11d1a..022763664f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java @@ -1,19 +1,3 @@ -/* - * Copyright 2023. the original author or authors. - * - * 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. - */ - /* * Copyright 2023 the original author or authors. * @@ -21,7 +5,7 @@ * 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 + * https://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, @@ -31,51 +15,28 @@ */ package org.springframework.data.mongodb.core.encryption; -import java.util.Collections; - -import com.mongodb.client.model.vault.DataKeyOptions; -import com.mongodb.client.vault.ClientEncryption; -import org.bson.BsonBinary; -import org.bson.BsonValue; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoValueConverter; -import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl - * @since 2023/02 + * @since 4.1 */ -public class EncryptingConverter implements MongoValueConverter { - - private ClientEncryption clientEncryption; - private BsonBinary dataKeyId; // should be provided from outside. - - public EncryptingConverter(ClientEncryption clientEncryption) { +public interface EncryptingConverter extends MongoValueConverter { - this.clientEncryption = clientEncryption; - this.dataKeyId = clientEncryption.createDataKey("local", - new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))); + @Override + default S read(Object value, MongoConversionContext context) { + return decrypt(value, buildEncryptionContext(context)); } - @Nullable @Override - public Object read(Object value, MongoConversionContext context) { - - ManualEncryptionContext encryptionContext = buildEncryptionContext(context); - Object decrypted = encryptionContext.decrypt(value, clientEncryption); - return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; + default T write(Object value, MongoConversionContext context) { + return encrypt(value, buildEncryptionContext(context)); } - @Nullable - @Override - public BsonBinary write(Object value, MongoConversionContext context) { + S decrypt(Object value, EncryptionContext context); - ManualEncryptionContext encryptionContext = buildEncryptionContext(context); - return encryptionContext.encrypt(value, clientEncryption); - } + T encrypt(Object value, EncryptionContext context); - ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { - return new ManualEncryptionContext(context, this.dataKeyId); - } + EncryptionContext buildEncryptionContext(MongoConversionContext context); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java new file mode 100644 index 0000000000..f6f604b64a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -0,0 +1,46 @@ +/* + * Copyright 2023. the original author or authors. + * + * 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 org.springframework.data.mongodb.core.encryption; + +import org.springframework.data.convert.ValueConversionContext; +import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; + +/** + * @author Christoph Strobl + */ +public interface EncryptionContext extends ValueConversionContext { + + default String getAlgorithm() { + Encrypted annotation = getProperty().findAnnotation(Encrypted.class); + if(annotation == null) { + throw new IllegalStateException("Not an encrypted property"); + } + return annotation.algorithm(); + } + + default boolean isExplicitlyEncrypted() { + return getProperty().isAnnotationPresent(ExplicitlyEncrypted.class); + } + + default Object getKeyId() { + Encrypted annotation = getProperty().findAnnotation(Encrypted.class); + if(annotation == null) { + throw new IllegalStateException("Not an encrypted property"); + } + return annotation.keyId(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java index 90fe3b528b..67b1978f3d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java @@ -1,19 +1,3 @@ -/* - * Copyright 2023. the original author or authors. - * - * 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. - */ - /* * Copyright 2023 the original author or authors. * @@ -21,7 +5,7 @@ * 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 + * https://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, @@ -38,13 +22,12 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.data.convert.PropertyValueConverter; -import org.springframework.data.convert.PropertyValueConverter.ObjectToObjectPropertyValueConverter; import org.springframework.data.convert.ValueConverter; import org.springframework.data.mongodb.core.mapping.Encrypted; /** * @author Christoph Strobl - * @since 2023/02 + * @since 4.1 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @@ -58,5 +41,5 @@ String altKeyName() default ""; @AliasFor(annotation = ValueConverter.class, value = "value") - Class value() default EncryptingConverter.class; + Class value() default ClientEncryptionConverter.class; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java new file mode 100644 index 0000000000..a2639d8be3 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; + +/** + * @author Christoph Strobl + */ +public interface KeyIdProvider { + T getKeyId(MongoPersistentProperty property); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java index 47487edf52..b5ccbd428e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java @@ -54,18 +54,18 @@ * @author Christoph Strobl * @since 2023/02 */ -public class ManualEncryptionContext { +public class ManualEncryptionContext implements EncryptionContext { MongoConversionContext context; MongoPersistentProperty persistentProperty; - BsonBinary dataKeyId; + KeyIdProvider keyIdProvider; Lazy encryption; - public ManualEncryptionContext(MongoConversionContext context, BsonBinary dataKeyId) { + public ManualEncryptionContext(MongoConversionContext context, KeyIdProvider keyIdProvider) { this.context = context; this.persistentProperty = context.getProperty(); - this.dataKeyId = dataKeyId; this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); + this.keyIdProvider = keyIdProvider; } BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { @@ -84,7 +84,7 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); } } else { - encryptOptions = encryptOptions.keyId(this.dataKeyId); + encryptOptions = encryptOptions.keyId(keyIdProvider.getKeyId(persistentProperty)); } System.out.println( @@ -196,4 +196,9 @@ public Object decrypt(Object value, ClientEncryption clientEncryption) { return result; } + + @Override + public MongoPersistentProperty getProperty() { + return context.getProperty(); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index 0182cd8dba..f2971fe573 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -47,7 +47,7 @@ import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.encryption.ExplicitlyEncrypted; -import org.springframework.data.mongodb.core.encryption.EncryptingConverter; +import org.springframework.data.mongodb.core.encryption.ClientEncryptionConverter; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.fle.FLETests.Config; import org.springframework.test.context.ContextConfiguration; @@ -232,8 +232,8 @@ protected void configureConverters(MongoConverterConfigurationAdapter converterC } @Bean - EncryptingConverter encryptingConverter(ClientEncryption clientEncryption) { - return new EncryptingConverter(clientEncryption); + ClientEncryptionConverter encryptingConverter(ClientEncryption clientEncryption) { + return new ClientEncryptionConverter(clientEncryption, null); } @Bean From cc7d47d74ee7cdbf7c64b4f6d717755233921d63 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Feb 2023 07:46:53 +0100 Subject: [PATCH 10/24] Move stuff from manual encryption context to clientencryptionconverter --- .../encryption/ClientEncryptionConverter.java | 84 +++++++++++++++++-- .../core/encryption/EncryptionContext.java | 13 +++ .../encryption/ManualEncryptionContext.java | 60 +++++++------ 3 files changed, 117 insertions(+), 40 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java index 4db3831242..f851242cd5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -15,17 +15,24 @@ */ package org.springframework.data.mongodb.core.encryption; +import java.util.Collection; import java.util.Collections; +import com.mongodb.client.model.vault.EncryptOptions; +import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonValue; +import org.bson.Document; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; import com.mongodb.client.model.vault.DataKeyOptions; import com.mongodb.client.vault.ClientEncryption; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * @author Christoph Strobl @@ -56,14 +63,6 @@ public Object read(Object value, MongoConversionContext context) { return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; } - @Nullable - @Override - public BsonBinary write(Object value, MongoConversionContext context) { - - ManualEncryptionContext encryptionContext = buildEncryptionContext(context); - return encryptionContext.encrypt(value, clientEncryption); - } - @Override public Object decrypt(Object value, EncryptionContext context) { return null; @@ -71,6 +70,75 @@ public Object decrypt(Object value, EncryptionContext context) { @Override public Object encrypt(Object value, EncryptionContext context) { + + MongoPersistentProperty persistentProperty = context.getProperty(); + EncryptOptions encryptOptions = new EncryptOptions(context.getAlgorithm()); + + ExplicitlyEncrypted annotation = persistentProperty.findAnnotation(ExplicitlyEncrypted.class); + if (annotation != null && !annotation.altKeyName().isBlank()) { + if (annotation.altKeyName().startsWith("/")) { + String fieldName = annotation.altKeyName().replace("/", ""); + Object altKeyNameValue = context.lookupValue(fieldName); + encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); + } else { + encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); + } + } else { + encryptOptions = encryptOptions.keyId(keyIdProvider.getKeyId(persistentProperty)); + } + + System.out.println( + "encrypting with: " + (StringUtils.hasText(encryptOptions.getKeyAltName()) ? encryptOptions.getKeyAltName() + : encryptOptions.getKeyId())); + + if (!persistentProperty.isEntity()) { + + if (persistentProperty.isCollectionLike()) { + return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + } + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + } + if (persistentProperty.isCollectionLike()) { + return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + } + + Object write = context.getConversionContext().write(value); + if (write instanceof Document doc) { + return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); + } + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); + } + + public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, EncryptionContext context) { + + if (property.isCollectionLike()) { + + BsonArray bsonArray = new BsonArray(); + if (!property.isEntity()) { + if (value instanceof Collection values) { + values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + bsonArray.add(BsonUtils.simpleToBsonValue(o)); + } + } + return bsonArray; + } else { + if (value instanceof Collection values) { + values.forEach(it -> { + Document write = (Document) context.getConversionContext().write(it, property.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); + }); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + Document write = (Document) context.getConversionContext().write(o, property.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); + } + } + return bsonArray; + } + } + return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index f6f604b64a..15744f5414 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -16,8 +16,10 @@ package org.springframework.data.mongodb.core.encryption; import org.springframework.data.convert.ValueConversionContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.lang.Nullable; /** * @author Christoph Strobl @@ -43,4 +45,15 @@ default Object getKeyId() { } return annotation.keyId(); } + + @Nullable + default Object lookupValue(String path) { + return getConversionContext().getValue(path); + } + + MongoConversionContext getConversionContext(); + + default Object convertToMongoType(Object value) { + return getConversionContext().write(value); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java index b5ccbd428e..59f32a8640 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java @@ -1,19 +1,3 @@ -/* - * Copyright 2023. the original author or authors. - * - * 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. - */ - /* * Copyright 2023 the original author or authors. * @@ -21,7 +5,7 @@ * 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 + * https://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, @@ -47,23 +31,24 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.util.Lazy; +import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * @author Christoph Strobl - * @since 2023/02 + * @since 4.1 */ public class ManualEncryptionContext implements EncryptionContext { - MongoConversionContext context; - MongoPersistentProperty persistentProperty; - KeyIdProvider keyIdProvider; - Lazy encryption; + private final MongoConversionContext conversionContext; + private final MongoPersistentProperty persistentProperty; + private final KeyIdProvider keyIdProvider; + private final Lazy encryption; - public ManualEncryptionContext(MongoConversionContext context, KeyIdProvider keyIdProvider) { - this.context = context; - this.persistentProperty = context.getProperty(); + public ManualEncryptionContext(MongoConversionContext conversionContext, KeyIdProvider keyIdProvider) { + this.conversionContext = conversionContext; + this.persistentProperty = conversionContext.getProperty(); this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); this.keyIdProvider = keyIdProvider; } @@ -78,7 +63,7 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { if (annotation != null && !annotation.altKeyName().isBlank()) { if (annotation.altKeyName().startsWith("/")) { String fieldName = annotation.altKeyName().replace("/", ""); - Object altKeyNameValue = context.getValue(fieldName); + Object altKeyNameValue = conversionContext.getValue(fieldName); encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); } else { encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); @@ -102,7 +87,7 @@ BsonBinary encrypt(Object value, ClientEncryption clientEncryption) { return clientEncryption.encrypt(collectionLikeToBsonValue(value), encryptOptions); } - Object write = context.write(value); + Object write = conversionContext.write(value); if (write instanceof Document doc) { return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); } @@ -126,12 +111,12 @@ public BsonValue collectionLikeToBsonValue(Object value) { } else { if (value instanceof Collection values) { values.forEach(it -> { - Document write = (Document) context.write(it, persistentProperty.getTypeInformation()); + Document write = (Document) conversionContext.write(it, persistentProperty.getTypeInformation()); bsonArray.add(write.toBsonDocument()); }); } else if (ObjectUtils.isArray(value)) { for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) context.write(0, persistentProperty.getTypeInformation()); + Document write = (Document) conversionContext.write(0, persistentProperty.getTypeInformation()); bsonArray.add(write.toBsonDocument()); } } @@ -180,7 +165,7 @@ public Object decrypt(Object value, ClientEncryption clientEncryption) { } else { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); iterable.forEach(it -> { - collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); + collection.add(conversionContext.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); }); return collection; } @@ -191,7 +176,7 @@ public Object decrypt(Object value, ClientEncryption clientEncryption) { } if (persistentProperty.isEntity() && result instanceof BsonDocument bsonDocument) { - return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); + return conversionContext.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); } return result; @@ -199,6 +184,17 @@ public Object decrypt(Object value, ClientEncryption clientEncryption) { @Override public MongoPersistentProperty getProperty() { - return context.getProperty(); + return conversionContext.getProperty(); + } + + @Nullable + @Override + public Object lookupValue(String path) { + return conversionContext.getValue(path); + } + + @Override + public MongoConversionContext getConversionContext() { + return conversionContext; } } From 14ad6c3809849cd9f0e6e31c1339272229ee8b19 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Feb 2023 08:36:53 +0100 Subject: [PATCH 11/24] move decrypt to converter --- .../encryption/ClientEncryptionConverter.java | 65 ++++++++++++++++--- 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java index f851242cd5..926cd05963 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -17,12 +17,16 @@ import java.util.Collection; import java.util.Collections; +import java.util.function.Supplier; import com.mongodb.client.model.vault.EncryptOptions; import org.bson.BsonArray; import org.bson.BsonBinary; +import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.Document; +import org.bson.types.Binary; +import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; @@ -39,16 +43,20 @@ */ public class ClientEncryptionConverter implements EncryptingConverter { - private ClientEncryption clientEncryption; + private Supplier clientEncryption; private final KeyIdProvider keyIdProvider; public ClientEncryptionConverter(ClientEncryption clientEncryption, KeyIdProvider keyIdProvider) { + this(() -> clientEncryption, keyIdProvider); + } + + public ClientEncryptionConverter(Supplier clientEncryption, KeyIdProvider keyIdProvider) { this.clientEncryption = clientEncryption; if (keyIdProvider != null) { this.keyIdProvider = keyIdProvider; } else { - Lazy dataKey = Lazy.of(() -> clientEncryption.createDataKey("local", + Lazy dataKey = Lazy.of(() -> clientEncryption.get().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); this.keyIdProvider = (ctx) -> dataKey.get(); } @@ -58,14 +66,51 @@ public ClientEncryptionConverter(ClientEncryption clientEncryption, KeyIdProvide @Override public Object read(Object value, MongoConversionContext context) { - ManualEncryptionContext encryptionContext = buildEncryptionContext(context); - Object decrypted = encryptionContext.decrypt(value, clientEncryption); + Object decrypted = EncryptingConverter.super.read(value, context); return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; } @Override public Object decrypt(Object value, EncryptionContext context) { - return null; + + Object result = value; + if (value instanceof Binary binary) { + result = clientEncryption.get().decrypt(new BsonBinary(binary.getType(), binary.getData())); + } + if (value instanceof BsonBinary binary) { + result = clientEncryption.get().decrypt(binary); + } + + // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java + if (value == result) { + return result; + } + + MongoPersistentProperty persistentProperty = context.getProperty(); + if (context.getProperty().isCollectionLike() && result instanceof Iterable iterable) { + if (!persistentProperty.isEntity()) { + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); + return collection; + } else { + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + iterable.forEach(it -> { + collection.add(context.getConversionContext().read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); + }); + return collection; + } + } + + if (!persistentProperty.isEntity() && result instanceof BsonValue bsonValue) { + return BsonUtils.toJavaType(bsonValue); + } + + if (persistentProperty.isEntity() && result instanceof BsonDocument bsonDocument) { + return context.getConversionContext().read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); + } + + return result; } @Override @@ -94,19 +139,19 @@ public Object encrypt(Object value, EncryptionContext context) { if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { - return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return clientEncryption.get().encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); } - return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + return clientEncryption.get().encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); } if (persistentProperty.isCollectionLike()) { - return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return clientEncryption.get().encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); } Object write = context.getConversionContext().write(value); if (write instanceof Document doc) { - return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); + return clientEncryption.get().encrypt(doc.toBsonDocument(), encryptOptions); } - return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); + return clientEncryption.get().encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); } public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, EncryptionContext context) { From e01365d5de4d070ab6a65950d25f8f6acc1cb551 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Feb 2023 10:34:19 +0100 Subject: [PATCH 12/24] so this works, maybe we can move the keyid detection as well --- .../encryption/ClientEncryptionConverter.java | 84 +++++----- .../encryption/ClientEncryptionProvider.java | 127 +++++++++++++++ .../core/encryption/EncryptingConverter.java | 2 +- .../core/encryption/EncryptionContext.java | 22 ++- .../encryption/ManualEncryptionContext.java | 144 +----------------- .../data/mongodb/util/BsonUtils.java | 5 + .../data/mongodb/fle/FLETests.java | 45 +++--- 7 files changed, 211 insertions(+), 218 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java index 926cd05963..2ca3fec0bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -16,10 +16,7 @@ package org.springframework.data.mongodb.core.encryption; import java.util.Collection; -import java.util.Collections; -import java.util.function.Supplier; -import com.mongodb.client.model.vault.EncryptOptions; import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonDocument; @@ -30,36 +27,28 @@ import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; -import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.vault.ClientEncryption; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** * @author Christoph Strobl */ public class ClientEncryptionConverter implements EncryptingConverter { - private Supplier clientEncryption; + private ClientEncryptionProvider clientEncryption; private final KeyIdProvider keyIdProvider; public ClientEncryptionConverter(ClientEncryption clientEncryption, KeyIdProvider keyIdProvider) { - this(() -> clientEncryption, keyIdProvider); + this(ClientEncryptionProvider.just(clientEncryption), keyIdProvider); } - public ClientEncryptionConverter(Supplier clientEncryption, KeyIdProvider keyIdProvider) { + public ClientEncryptionConverter(ClientEncryptionProvider clientEncryption, KeyIdProvider keyIdProvider) { this.clientEncryption = clientEncryption; - if (keyIdProvider != null) { - this.keyIdProvider = keyIdProvider; - } else { - Lazy dataKey = Lazy.of(() -> clientEncryption.get().createDataKey("local", - new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); - this.keyIdProvider = (ctx) -> dataKey.get(); - } + this.keyIdProvider = keyIdProvider; } @Nullable @@ -71,24 +60,22 @@ public Object read(Object value, MongoConversionContext context) { } @Override - public Object decrypt(Object value, EncryptionContext context) { + public Object decrypt(Object encryptedValue, EncryptionContext context) { - Object result = value; - if (value instanceof Binary binary) { - result = clientEncryption.get().decrypt(new BsonBinary(binary.getType(), binary.getData())); - } - if (value instanceof BsonBinary binary) { - result = clientEncryption.get().decrypt(binary); - } + Object decryptedValue = encryptedValue; + if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) { - // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active - // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java - if (value == result) { - return result; + decryptedValue = clientEncryption.decrypt((BsonBinary) BsonUtils.simpleToBsonValue(encryptedValue)); + // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active + // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java + if (encryptedValue == decryptedValue) { + return decryptedValue; + } } MongoPersistentProperty persistentProperty = context.getProperty(); - if (context.getProperty().isCollectionLike() && result instanceof Iterable iterable) { + + if (context.getProperty().isCollectionLike() && decryptedValue instanceof Iterable iterable) { if (!persistentProperty.isEntity()) { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); @@ -96,21 +83,23 @@ public Object decrypt(Object value, EncryptionContext context) { } else { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); iterable.forEach(it -> { - collection.add(context.getConversionContext().read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); + collection.add(context.getSourceContext().read(BsonUtils.toJavaType((BsonValue) it), + persistentProperty.getActualType())); }); return collection; } } - if (!persistentProperty.isEntity() && result instanceof BsonValue bsonValue) { + if (!persistentProperty.isEntity() && decryptedValue instanceof BsonValue bsonValue) { return BsonUtils.toJavaType(bsonValue); } - if (persistentProperty.isEntity() && result instanceof BsonDocument bsonDocument) { - return context.getConversionContext().read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); + if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) { + return context.getSourceContext().read(BsonUtils.toJavaType(bsonDocument), + persistentProperty.getTypeInformation()); } - return result; + return decryptedValue; } @Override @@ -132,29 +121,28 @@ public Object encrypt(Object value, EncryptionContext context) { encryptOptions = encryptOptions.keyId(keyIdProvider.getKeyId(persistentProperty)); } - System.out.println( - "encrypting with: " + (StringUtils.hasText(encryptOptions.getKeyAltName()) ? encryptOptions.getKeyAltName() - : encryptOptions.getKeyId())); - if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { - return clientEncryption.get().encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return clientEncryption + .encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); } - return clientEncryption.get().encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); } if (persistentProperty.isCollectionLike()) { - return clientEncryption.get().encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return clientEncryption + .encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); } - Object write = context.getConversionContext().write(value); + Object write = context.getSourceContext().write(value); if (write instanceof Document doc) { - return clientEncryption.get().encrypt(doc.toBsonDocument(), encryptOptions); + return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); } - return clientEncryption.get().encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); + return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); } - public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, EncryptionContext context) { + public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, + EncryptionContext context) { if (property.isCollectionLike()) { @@ -171,12 +159,12 @@ public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty } else { if (value instanceof Collection values) { values.forEach(it -> { - Document write = (Document) context.getConversionContext().write(it, property.getTypeInformation()); + Document write = (Document) context.getSourceContext().write(it, property.getTypeInformation()); bsonArray.add(write.toBsonDocument()); }); } else if (ObjectUtils.isArray(value)) { for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) context.getConversionContext().write(o, property.getTypeInformation()); + Document write = (Document) context.getSourceContext().write(o, property.getTypeInformation()); bsonArray.add(write.toBsonDocument()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java new file mode 100644 index 0000000000..7757623235 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java @@ -0,0 +1,127 @@ +/* + * Copyright 2023. the original author or authors. + * + * 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. + */ + +/* + * Copyright 2023 the original author or authors. + * + * 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 org.springframework.data.mongodb.core.encryption; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.bson.BsonBinary; +import org.bson.BsonValue; + +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; + +/** + * @author Christoph Strobl + * @since 4.1 + */ +public interface ClientEncryptionProvider { + + ClientEncryption getClientEncryption(); + + boolean refresh(); + + void shutdown(); + + default BsonValue decrypt(BsonBinary value) { + return getClientEncryption().decrypt(value); + } + + default BsonBinary encrypt(BsonValue value, EncryptOptions options) { + return getClientEncryption().encrypt(value, options); + } + + static ClientEncryptionProvider from(Supplier clientEncryption) { + return new ClientEncryptionProvider() { + @Override + public ClientEncryption getClientEncryption() { + return clientEncryption.get(); + } + + @Override + public boolean refresh() { + return true; + } + + @Override + public void shutdown() { + clientEncryption.get().close(); + } + }; + } + + static ClientEncryptionProvider caching(Supplier source) { + + return new ClientEncryptionProvider() { + + final AtomicReference enc = new AtomicReference<>(source.get()); + + @Override + public ClientEncryption getClientEncryption() { + return enc.get(); + } + + @Override + public boolean refresh() { + enc.set(source.get()); + return true; + } + + @Override + public void shutdown() { + source.get().close(); + } + }; + } + + static ClientEncryptionProvider just(ClientEncryption clientEncryption) { + + return new ClientEncryptionProvider() { + + @Override + public ClientEncryption getClientEncryption() { + return clientEncryption; + } + + @Override + public boolean refresh() { + return false; + } + + @Override + public void shutdown() { + clientEncryption.close(); + } + }; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java index 022763664f..a3032e8bac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java @@ -34,7 +34,7 @@ default T write(Object value, MongoConversionContext context) { return encrypt(value, buildEncryptionContext(context)); } - S decrypt(Object value, EncryptionContext context); + S decrypt(Object encryptedValue, EncryptionContext context); T encrypt(Object value, EncryptionContext context); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 15744f5414..719d11ef58 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -28,8 +28,11 @@ public interface EncryptionContext extends ValueConversionContext bsonArray.add(BsonUtils.simpleToBsonValue(it))); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - bsonArray.add(BsonUtils.simpleToBsonValue(o)); - } - } - return bsonArray; - } else { - if (value instanceof Collection values) { - values.forEach(it -> { - Document write = (Document) conversionContext.write(it, persistentProperty.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); - }); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) conversionContext.write(0, persistentProperty.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); - } - } - return bsonArray; - } - } - - if (!persistentProperty.isEntity()) { - if (persistentProperty.isCollectionLike()) { - - if (persistentProperty.isEntity()) { - - } - } - } - - return null; - } - - public Object decrypt(Object value, ClientEncryption clientEncryption) { - - // this was a hack to avoid the 60 sec timeout of the key cache - // ClientEncryptionSettings settings = (ClientEncryptionSettings) new DirectFieldAccessor(clientEncryption) - // .getPropertyValue("options"); - // clientEncryption = ClientEncryptions.create(settings); - - Object result = value; - if (value instanceof Binary binary) { - result = clientEncryption.decrypt(new BsonBinary(binary.getType(), binary.getData())); - } - if (value instanceof BsonBinary binary) { - result = clientEncryption.decrypt(binary); - } - - // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active - // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java - if (value == result) { - return result; - } - - if (persistentProperty.isCollectionLike() && result instanceof Iterable iterable) { - if (!persistentProperty.isEntity()) { - Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); - iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); - return collection; - } else { - Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); - iterable.forEach(it -> { - collection.add(conversionContext.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); - }); - return collection; - } - } - - if (!persistentProperty.isEntity() && result instanceof BsonValue bsonValue) { - return BsonUtils.toJavaType(bsonValue); - } - - if (persistentProperty.isEntity() && result instanceof BsonDocument bsonDocument) { - return conversionContext.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); - } - - return result; - } - @Override public MongoPersistentProperty getProperty() { return conversionContext.getProperty(); @@ -194,7 +52,7 @@ public Object lookupValue(String path) { } @Override - public MongoConversionContext getConversionContext() { + public MongoConversionContext getSourceContext() { return conversionContext; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index 1e1ebe4af8..b8d4093f75 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -38,6 +38,7 @@ import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.bson.json.JsonParseException; +import org.bson.types.Binary; import org.bson.types.ObjectId; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.CodecRegistryProvider; @@ -370,6 +371,10 @@ public static BsonValue simpleToBsonValue(Object source) { return new BsonDouble((Float) source); } + if(source instanceof Binary binary) { + return new BsonBinary(binary.getType(), binary.getData()); + } + throw new IllegalArgumentException(String.format("Unable to convert %s (%s) to BsonValue.", source, source != null ? source.getClass().getName() : "null")); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index f2971fe573..3eead6df1e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -29,12 +29,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; import org.bson.BsonBinary; import org.bson.Document; import org.bson.types.Binary; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -46,10 +44,12 @@ import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; -import org.springframework.data.mongodb.core.encryption.ExplicitlyEncrypted; import org.springframework.data.mongodb.core.encryption.ClientEncryptionConverter; +import org.springframework.data.mongodb.core.encryption.ClientEncryptionProvider; +import org.springframework.data.mongodb.core.encryption.ExplicitlyEncrypted; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.fle.FLETests.Config; +import org.springframework.data.util.Lazy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -64,7 +64,6 @@ import com.mongodb.client.model.Indexes; import com.mongodb.client.model.vault.DataKeyOptions; import com.mongodb.client.result.DeleteResult; -import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryptions; /** @@ -148,7 +147,8 @@ void theUpdateStuff() { }); System.out.println("saved: " + savedDocument.toJson()); - template.update(Person.class).matching(where("id").is(person.id)).apply(Update.update("ssn", "secret-value")).first(); + template.update(Person.class).matching(where("id").is(person.id)).apply(Update.update("ssn", "secret-value")) + .first(); savedDocument = template.execute(Person.class, collection -> { return collection.find(new Document()).first(); @@ -159,13 +159,13 @@ void theUpdateStuff() { } @Test - @Disabled("for now - takes to long ") - void altKeyDetection(@Autowired ClientEncryption clientEncryption) throws InterruptedException { + // @Disabled("for now - takes to long ") + void altKeyDetection(@Autowired ClientEncryptionProvider clientEncryptionProvider) throws InterruptedException { - BsonBinary user1key = clientEncryption.createDataKey("local", + BsonBinary user1key = clientEncryptionProvider.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("user-1"))); - BsonBinary user2key = clientEncryption.createDataKey("local", + BsonBinary user2key = clientEncryptionProvider.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("user-2"))); Person p1 = new Person(); @@ -196,12 +196,13 @@ void altKeyDetection(@Autowired ClientEncryption clientEncryption) throws Interr // System.out.println(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()); // System.out.println(template.query(Person.class).matching(where("id").is(p2.id)).firstValue()); - DeleteResult deleteResult = clientEncryption.deleteKey(user2key); - clientEncryption.getKeys().forEach(System.out::println); + DeleteResult deleteResult = clientEncryptionProvider.getClientEncryption().deleteKey(user2key); + clientEncryptionProvider.getClientEncryption().getKeys().forEach(System.out::println); System.out.println("deleteResult: " + deleteResult); - System.out.println("---- waiting for cache timeout ----"); - TimeUnit.SECONDS.sleep(90); + // System.out.println("---- waiting for cache timeout ----"); + // TimeUnit.SECONDS.sleep(90); + clientEncryptionProvider.refresh(); assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1); @@ -232,12 +233,21 @@ protected void configureConverters(MongoConverterConfigurationAdapter converterC } @Bean - ClientEncryptionConverter encryptingConverter(ClientEncryption clientEncryption) { - return new ClientEncryptionConverter(clientEncryption, null); + ClientEncryptionConverter encryptingConverter(ClientEncryptionProvider encryptionProvider) { + + Lazy dataKey = Lazy.of(() -> encryptionProvider.getClientEncryption().createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); + + return new ClientEncryptionConverter(encryptionProvider, (ctx) -> dataKey.get()); + } + + @Bean + ClientEncryptionProvider clientEncryption(ClientEncryptionSettings encryptionSettings) { + return ClientEncryptionProvider.caching(() -> ClientEncryptions.create(encryptionSettings)); } @Bean - ClientEncryption clientEncryption(MongoClient mongoClient) { + ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) { final byte[] localMasterKey = new byte[96]; new SecureRandom().nextBytes(localMasterKey); @@ -267,8 +277,7 @@ ClientEncryption clientEncryption(MongoClient mongoClient) { .keyVaultMongoClientSettings( MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) .keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build(); - ClientEncryption clientEncryption = ClientEncryptions.create(clientEncryptionSettings); - return clientEncryption; + return clientEncryptionSettings; } } From bab9e66377a2dafd854d985e3d651752c1f3107e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Feb 2023 11:20:39 +0100 Subject: [PATCH 13/24] introduce key provider api --- .../encryption/ClientEncryptionConverter.java | 47 +++++++++------- .../core/encryption/EncryptionKey.java | 56 +++++++++++++++++++ .../encryption/EncryptionKeyProvider.java | 51 +++++++++++++++++ .../core/encryption/KeyIdProvider.java | 25 --------- .../encryption/ManualEncryptionContext.java | 6 +- .../data/mongodb/fle/FLETests.java | 8 ++- 6 files changed, 141 insertions(+), 52 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java index 2ca3fec0bd..113abf65e0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -25,6 +25,7 @@ import org.bson.types.Binary; import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; @@ -39,16 +40,16 @@ public class ClientEncryptionConverter implements EncryptingConverter { private ClientEncryptionProvider clientEncryption; - private final KeyIdProvider keyIdProvider; + private final EncryptionKeyProvider keyProvider; - public ClientEncryptionConverter(ClientEncryption clientEncryption, KeyIdProvider keyIdProvider) { - this(ClientEncryptionProvider.just(clientEncryption), keyIdProvider); + public ClientEncryptionConverter(ClientEncryption clientEncryption, EncryptionKeyProvider keyProvider) { + this(ClientEncryptionProvider.just(clientEncryption), keyProvider); } - public ClientEncryptionConverter(ClientEncryptionProvider clientEncryption, KeyIdProvider keyIdProvider) { + public ClientEncryptionConverter(ClientEncryptionProvider clientEncryption, EncryptionKeyProvider keyProvider) { this.clientEncryption = clientEncryption; - this.keyIdProvider = keyIdProvider; + this.keyProvider = keyProvider; } @Nullable @@ -108,30 +109,34 @@ public Object encrypt(Object value, EncryptionContext context) { MongoPersistentProperty persistentProperty = context.getProperty(); EncryptOptions encryptOptions = new EncryptOptions(context.getAlgorithm()); - ExplicitlyEncrypted annotation = persistentProperty.findAnnotation(ExplicitlyEncrypted.class); - if (annotation != null && !annotation.altKeyName().isBlank()) { - if (annotation.altKeyName().startsWith("/")) { - String fieldName = annotation.altKeyName().replace("/", ""); - Object altKeyNameValue = context.lookupValue(fieldName); - encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); - } else { - encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); - } + // ExplicitlyEncrypted annotation = persistentProperty.findAnnotation(ExplicitlyEncrypted.class); + // if (annotation != null && !annotation.altKeyName().isBlank()) { + // if (annotation.altKeyName().startsWith("/")) { + // String fieldName = annotation.altKeyName().replace("/", ""); + // Object altKeyNameValue = context.lookupValue(fieldName); + // encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); + // } else { + // encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); + // } + // } else { + // encryptOptions = encryptOptions.keyId(encryptionKeyProvider.getKey(persistentProperty, context)); + // } + + EncryptionKey key = keyProvider.getKey(context); + if (Type.ALT.equals(key.type())) { + encryptOptions = encryptOptions.keyAltName(key.key().toString()); } else { - encryptOptions = encryptOptions.keyId(keyIdProvider.getKeyId(persistentProperty)); + encryptOptions = encryptOptions.keyId((BsonBinary) key.key()); } - if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { - return clientEncryption - .encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); } return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); } if (persistentProperty.isCollectionLike()) { - return clientEncryption - .encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); } Object write = context.getSourceContext().write(value); @@ -176,6 +181,6 @@ public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty } public ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { - return new ManualEncryptionContext(context, this.keyIdProvider); + return new ManualEncryptionContext(context, this.keyProvider); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java new file mode 100644 index 0000000000..a1dfd1b300 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023. the original author or authors. + * + * 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 org.springframework.data.mongodb.core.encryption; + +import org.bson.BsonBinary; + +/** + * @author Christoph Strobl + * @since 4.1 + */ +public interface EncryptionKey { + + Object key(); + Type type(); + + static KeyId keyId(BsonBinary key) { + return new KeyId(key); + } + + static AltKeyName altKeyName(String altKeyName) { + return new AltKeyName(altKeyName); + } + + enum Type { + ID, ALT + } + + record KeyId(BsonBinary key) implements EncryptionKey { + + @Override + public Type type() { + return Type.ID; + } + } + + record AltKeyName(String key) implements EncryptionKey { + + @Override + public Type type() { + return Type.ALT; + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java new file mode 100644 index 0000000000..3d7e710837 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java @@ -0,0 +1,51 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import java.util.function.Supplier; + +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +public interface EncryptionKeyProvider { + + EncryptionKey getKey(EncryptionContext encryptionContext); + + static EncryptionKeyProvider annotationBasedKeyProvider(Supplier fallback) { + + return ((encryptionContext) -> { + + ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); + if (annotation != null && !annotation.altKeyName().isBlank()) { + if (annotation.altKeyName().startsWith("/")) { + String fieldName = annotation.altKeyName().replace("/", ""); + return new EncryptionKey.AltKeyName(encryptionContext.lookupValue(fieldName).toString()); + } else { + return new EncryptionKey.AltKeyName(annotation.altKeyName()); + } + } + if (encryptionContext.getKeyId() != null && !ObjectUtils.isEmpty(encryptionContext.getKeyId())) { + // TODO: resolve the hash + // return EncryptionKey.keyId() + throw new IllegalStateException(); + } + return fallback.get(); + }); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java deleted file mode 100644 index a2639d8be3..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyIdProvider.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * 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 - * - * https://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 org.springframework.data.mongodb.core.encryption; - -import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; - -/** - * @author Christoph Strobl - */ -public interface KeyIdProvider { - T getKeyId(MongoPersistentProperty property); -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java index b9d3a65e80..e7513790ee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java @@ -30,14 +30,14 @@ public class ManualEncryptionContext implements EncryptionContext { private final MongoConversionContext conversionContext; private final MongoPersistentProperty persistentProperty; - private final KeyIdProvider keyIdProvider; + private final EncryptionKeyProvider encryptionKeyProvider; private final Lazy encryption; - public ManualEncryptionContext(MongoConversionContext conversionContext, KeyIdProvider keyIdProvider) { + public ManualEncryptionContext(MongoConversionContext conversionContext, EncryptionKeyProvider encryptionKeyProvider) { this.conversionContext = conversionContext; this.persistentProperty = conversionContext.getProperty(); this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); - this.keyIdProvider = keyIdProvider; + this.encryptionKeyProvider = encryptionKeyProvider; } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index 3eead6df1e..fe87430f7c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -46,6 +46,8 @@ import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.encryption.ClientEncryptionConverter; import org.springframework.data.mongodb.core.encryption.ClientEncryptionProvider; +import org.springframework.data.mongodb.core.encryption.EncryptionKey; +import org.springframework.data.mongodb.core.encryption.EncryptionKeyProvider; import org.springframework.data.mongodb.core.encryption.ExplicitlyEncrypted; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.fle.FLETests.Config; @@ -233,12 +235,12 @@ protected void configureConverters(MongoConverterConfigurationAdapter converterC } @Bean - ClientEncryptionConverter encryptingConverter(ClientEncryptionProvider encryptionProvider) { + ClientEncryptionConverter encryptingConverter(ClientEncryptionProvider clientEncryptionProvider) { - Lazy dataKey = Lazy.of(() -> encryptionProvider.getClientEncryption().createDataKey("local", + Lazy dataKey = Lazy.of(() -> clientEncryptionProvider.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); - return new ClientEncryptionConverter(encryptionProvider, (ctx) -> dataKey.get()); + return new ClientEncryptionConverter(clientEncryptionProvider, EncryptionKeyProvider.annotationBasedKeyProvider(() -> EncryptionKey.keyId(dataKey.get()))); } @Bean From 53d502993a4edf1e94b51eb61ae1ef94107367eb Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Feb 2023 12:21:11 +0100 Subject: [PATCH 14/24] add test for typed aggregation --- .../encryption/ClientEncryptionConverter.java | 17 ++------- .../core/encryption/EncryptionContext.java | 25 +++++-------- .../core/encryption/EncryptionKey.java | 15 ++++---- .../encryption/EncryptionKeyProvider.java | 35 ++++++++++++------- .../data/mongodb/fle/FLETests.java | 21 +++++++++-- 5 files changed, 60 insertions(+), 53 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java index 113abf65e0..16c15fab6a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -109,24 +109,11 @@ public Object encrypt(Object value, EncryptionContext context) { MongoPersistentProperty persistentProperty = context.getProperty(); EncryptOptions encryptOptions = new EncryptOptions(context.getAlgorithm()); - // ExplicitlyEncrypted annotation = persistentProperty.findAnnotation(ExplicitlyEncrypted.class); - // if (annotation != null && !annotation.altKeyName().isBlank()) { - // if (annotation.altKeyName().startsWith("/")) { - // String fieldName = annotation.altKeyName().replace("/", ""); - // Object altKeyNameValue = context.lookupValue(fieldName); - // encryptOptions = encryptOptions.keyAltName(altKeyNameValue.toString()); - // } else { - // encryptOptions = encryptOptions.keyAltName(annotation.altKeyName()); - // } - // } else { - // encryptOptions = encryptOptions.keyId(encryptionKeyProvider.getKey(persistentProperty, context)); - // } - EncryptionKey key = keyProvider.getKey(context); if (Type.ALT.equals(key.type())) { - encryptOptions = encryptOptions.keyAltName(key.key().toString()); + encryptOptions = encryptOptions.keyAltName(key.value().toString()); } else { - encryptOptions = encryptOptions.keyId((BsonBinary) key.key()); + encryptOptions = encryptOptions.keyId((BsonBinary) key.value()); } if (!persistentProperty.isEntity()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 719d11ef58..bf7472201c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -27,12 +27,9 @@ public interface EncryptionContext extends ValueConversionContext { default String getAlgorithm() { - Encrypted annotation = getProperty().findAnnotation(Encrypted.class); + Encrypted annotation = lookupEncryptedAnnotation(); if (annotation == null) { - annotation = getProperty().getOwner().findAnnotation(Encrypted.class); - if (annotation == null) { - throw new IllegalStateException("Not an encrypted property"); - } + throw new IllegalStateException("No algorithm defined for property " + getProperty().getName()); } return annotation.algorithm(); } @@ -41,17 +38,6 @@ default boolean isExplicitlyEncrypted() { return getProperty().isAnnotationPresent(ExplicitlyEncrypted.class); } - default Object getKeyId() { - Encrypted annotation = getProperty().findAnnotation(Encrypted.class); - if (annotation == null) { - annotation = getProperty().getOwner().findAnnotation(Encrypted.class); - if (annotation == null) { - throw new IllegalStateException("Not an encrypted property"); - } - } - return annotation.keyId(); - } - @Nullable default Object lookupValue(String path) { return getSourceContext().getValue(path); @@ -61,5 +47,12 @@ default Object convertToMongoType(Object value) { return getSourceContext().write(value); } + default Encrypted lookupEncryptedAnnotation() { + + // TODO: having the path present here would really be helpful to inherit the algorithm + Encrypted annotation = getProperty().findAnnotation(Encrypted.class); + return annotation != null ? annotation : getProperty().getOwner().findAnnotation(Encrypted.class); + } + MongoConversionContext getSourceContext(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java index a1dfd1b300..3fec05c3ff 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java @@ -23,7 +23,8 @@ */ public interface EncryptionKey { - Object key(); + Object value(); + Type type(); static KeyId keyId(BsonBinary key) { @@ -34,11 +35,7 @@ static AltKeyName altKeyName(String altKeyName) { return new AltKeyName(altKeyName); } - enum Type { - ID, ALT - } - - record KeyId(BsonBinary key) implements EncryptionKey { + record KeyId(BsonBinary value) implements EncryptionKey { @Override public Type type() { @@ -46,11 +43,15 @@ public Type type() { } } - record AltKeyName(String key) implements EncryptionKey { + record AltKeyName(String value) implements EncryptionKey { @Override public Type type() { return Type.ALT; } } + + enum Type { + ID, ALT + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java index 3d7e710837..6f86348b5e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java @@ -17,7 +17,6 @@ import java.util.function.Supplier; -import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -27,24 +26,34 @@ public interface EncryptionKeyProvider { EncryptionKey getKey(EncryptionContext encryptionContext); + static EncryptionKeyProvider annotationBasedKeyProvider() { + return annotationBasedKeyProvider(() -> { + throw new IllegalStateException("No Encryption key found"); + }); + } + static EncryptionKeyProvider annotationBasedKeyProvider(Supplier fallback) { return ((encryptionContext) -> { - ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); - if (annotation != null && !annotation.altKeyName().isBlank()) { - if (annotation.altKeyName().startsWith("/")) { - String fieldName = annotation.altKeyName().replace("/", ""); - return new EncryptionKey.AltKeyName(encryptionContext.lookupValue(fieldName).toString()); - } else { - return new EncryptionKey.AltKeyName(annotation.altKeyName()); + if (encryptionContext.isExplicitlyEncrypted()) { + + ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); + if (StringUtils.hasText(annotation.altKeyName())) { + String altKeyName = annotation.altKeyName(); + if (altKeyName.startsWith("/")) { + String fieldName = altKeyName.replace("/", ""); + return new EncryptionKey.AltKeyName(encryptionContext.lookupValue(fieldName).toString()); + } else { + return new EncryptionKey.AltKeyName(altKeyName); + } } } - if (encryptionContext.getKeyId() != null && !ObjectUtils.isEmpty(encryptionContext.getKeyId())) { - // TODO: resolve the hash - // return EncryptionKey.keyId() - throw new IllegalStateException(); - } + // if (encryptionContext.getKeyId() != null && !ObjectUtils.isEmpty(encryptionContext.getKeyId())) { + // // TODO: resolve the hash + // // return EncryptionKey.keyId() + // throw new IllegalStateException(); + // } return fallback.get(); }); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index fe87430f7c..bfa14c65a3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.*; import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import lombok.Data; @@ -43,6 +44,8 @@ import org.springframework.data.convert.PropertyValueConverterFactory; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.encryption.ClientEncryptionConverter; import org.springframework.data.mongodb.core.encryption.ClientEncryptionProvider; @@ -157,11 +160,24 @@ void theUpdateStuff() { }); System.out.println("updated: " + savedDocument.toJson()); assertThat(savedDocument.get("ssn")).isInstanceOf(Binary.class); + } + + @Test + void aggregationWithMatch() { + + Person person = new Person(); + person.id = "id-1"; + person.name = "p1-name"; + person.ssn = "mySecretSSN"; + + template.save(person); + AggregationResults aggregationResults = template.aggregateAndReturn(Person.class) + .by(newAggregation(Person.class, Aggregation.match(where("ssn").is(person.ssn)))).all(); + assertThat(aggregationResults.getMappedResults()).containsExactly(person); } @Test - // @Disabled("for now - takes to long ") void altKeyDetection(@Autowired ClientEncryptionProvider clientEncryptionProvider) throws InterruptedException { BsonBinary user1key = clientEncryptionProvider.getClientEncryption().createDataKey("local", @@ -240,7 +256,8 @@ ClientEncryptionConverter encryptingConverter(ClientEncryptionProvider clientEnc Lazy dataKey = Lazy.of(() -> clientEncryptionProvider.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); - return new ClientEncryptionConverter(clientEncryptionProvider, EncryptionKeyProvider.annotationBasedKeyProvider(() -> EncryptionKey.keyId(dataKey.get()))); + return new ClientEncryptionConverter(clientEncryptionProvider, + EncryptionKeyProvider.annotationBasedKeyProvider(() -> EncryptionKey.keyId(dataKey.get()))); } @Bean From 1d83fa32d71e6b7a643857e4abf6c7275ab83c99 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 14 Feb 2023 14:41:30 +0100 Subject: [PATCH 15/24] Moooar abstractions --- .../encryption/ClientEncryptionConverter.java | 57 ++++---- .../encryption/ClientEncryptionProvider.java | 127 ------------------ .../core/encryption/EncryptingConverter.java | 2 + .../mongodb/core/encryption/Encryption.java | 28 ++++ .../core/encryption/EncryptionContext.java | 12 +- .../core/encryption/EncryptionKey.java | 45 ++++++- .../encryption/EncryptionKeyProvider.java | 60 --------- .../encryption/EncryptionKeyResolver.java | 56 ++++++++ .../core/encryption/EncryptionOptions.java | 44 ++++++ ...xt.java => ExplicitEncryptionContext.java} | 13 +- .../core/encryption/ExplicitlyEncrypted.java | 47 +++++++ .../encryption/MongoClientEncryption.java | 80 +++++++++++ .../data/mongodb/fle/FLETests.java | 50 +++---- 13 files changed, 359 insertions(+), 262 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/{ManualEncryptionContext.java => ExplicitEncryptionContext.java} (66%) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java index 16c15fab6a..b5768c4ac4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -17,6 +17,8 @@ import java.util.Collection; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.bson.BsonArray; import org.bson.BsonBinary; import org.bson.BsonDocument; @@ -25,30 +27,26 @@ import org.bson.types.Binary; import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; -import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; -import com.mongodb.client.model.vault.EncryptOptions; -import com.mongodb.client.vault.ClientEncryption; - /** * @author Christoph Strobl + * @since 4.1 */ public class ClientEncryptionConverter implements EncryptingConverter { - private ClientEncryptionProvider clientEncryption; - private final EncryptionKeyProvider keyProvider; + private static final Log LOGGER = LogFactory.getLog(ClientEncryptionConverter.class); - public ClientEncryptionConverter(ClientEncryption clientEncryption, EncryptionKeyProvider keyProvider) { - this(ClientEncryptionProvider.just(clientEncryption), keyProvider); - } + private Encryption encryptionProvider; + private final EncryptionKeyResolver keyProvider; - public ClientEncryptionConverter(ClientEncryptionProvider clientEncryption, EncryptionKeyProvider keyProvider) { + public ClientEncryptionConverter(Encryption encryptionProvider, + EncryptionKeyResolver keyProvider) { - this.clientEncryption = clientEncryption; + this.encryptionProvider = encryptionProvider; this.keyProvider = keyProvider; } @@ -66,7 +64,12 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { Object decryptedValue = encryptedValue; if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) { - decryptedValue = clientEncryption.decrypt((BsonBinary) BsonUtils.simpleToBsonValue(encryptedValue)); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("Decrypting %s.%s.", context.getProperty().getOwner().getName(), + context.getProperty().getName())); + } + + decryptedValue = encryptionProvider.decrypt((BsonBinary) BsonUtils.simpleToBsonValue(encryptedValue)); // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java if (encryptedValue == decryptedValue) { @@ -106,31 +109,33 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { @Override public Object encrypt(Object value, EncryptionContext context) { + if (LOGGER.isDebugEnabled()) { + LOGGER.debug(String.format("Encrypting %s.%s.", context.getProperty().getOwner().getName(), + context.getProperty().getName())); + } + MongoPersistentProperty persistentProperty = context.getProperty(); - EncryptOptions encryptOptions = new EncryptOptions(context.getAlgorithm()); + EncryptionOptions encryptionOptions = new EncryptionOptions(context.lookupEncryptedAnnotation().algorithm()); + encryptionOptions.setKey(keyProvider.getKey(context)); - EncryptionKey key = keyProvider.getKey(context); - if (Type.ALT.equals(key.type())) { - encryptOptions = encryptOptions.keyAltName(key.value().toString()); - } else { - encryptOptions = encryptOptions.keyId((BsonBinary) key.value()); - } if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { - return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return encryptionProvider.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), + encryptionOptions); } - return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptOptions); + return encryptionProvider.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions); } if (persistentProperty.isCollectionLike()) { - return clientEncryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptOptions); + return encryptionProvider.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), + encryptionOptions); } Object write = context.getSourceContext().write(value); if (write instanceof Document doc) { - return clientEncryption.encrypt(doc.toBsonDocument(), encryptOptions); + return encryptionProvider.encrypt(doc.toBsonDocument(), encryptionOptions); } - return clientEncryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptOptions); + return encryptionProvider.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions); } public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, @@ -167,7 +172,7 @@ public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty return null; } - public ManualEncryptionContext buildEncryptionContext(MongoConversionContext context) { - return new ManualEncryptionContext(context, this.keyProvider); + public EncryptionContext buildEncryptionContext(MongoConversionContext context) { + return new ExplicitEncryptionContext(context); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java deleted file mode 100644 index 7757623235..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionProvider.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2023. the original author or authors. - * - * 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. - */ - -/* - * Copyright 2023 the original author or authors. - * - * 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 org.springframework.data.mongodb.core.encryption; - -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Supplier; - -import org.bson.BsonBinary; -import org.bson.BsonValue; - -import com.mongodb.client.model.vault.EncryptOptions; -import com.mongodb.client.vault.ClientEncryption; - -/** - * @author Christoph Strobl - * @since 4.1 - */ -public interface ClientEncryptionProvider { - - ClientEncryption getClientEncryption(); - - boolean refresh(); - - void shutdown(); - - default BsonValue decrypt(BsonBinary value) { - return getClientEncryption().decrypt(value); - } - - default BsonBinary encrypt(BsonValue value, EncryptOptions options) { - return getClientEncryption().encrypt(value, options); - } - - static ClientEncryptionProvider from(Supplier clientEncryption) { - return new ClientEncryptionProvider() { - @Override - public ClientEncryption getClientEncryption() { - return clientEncryption.get(); - } - - @Override - public boolean refresh() { - return true; - } - - @Override - public void shutdown() { - clientEncryption.get().close(); - } - }; - } - - static ClientEncryptionProvider caching(Supplier source) { - - return new ClientEncryptionProvider() { - - final AtomicReference enc = new AtomicReference<>(source.get()); - - @Override - public ClientEncryption getClientEncryption() { - return enc.get(); - } - - @Override - public boolean refresh() { - enc.set(source.get()); - return true; - } - - @Override - public void shutdown() { - source.get().close(); - } - }; - } - - static ClientEncryptionProvider just(ClientEncryption clientEncryption) { - - return new ClientEncryptionProvider() { - - @Override - public ClientEncryption getClientEncryption() { - return clientEncryption; - } - - @Override - public boolean refresh() { - return false; - } - - @Override - public void shutdown() { - clientEncryption.close(); - } - }; - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java index a3032e8bac..3067366b00 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java @@ -19,6 +19,8 @@ import org.springframework.data.mongodb.core.convert.MongoValueConverter; /** + * A specialized {@link MongoValueConverter} for {@literal en-/decrypting} properties. + * * @author Christoph Strobl * @since 4.1 */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java new file mode 100644 index 0000000000..717b9d7e00 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java @@ -0,0 +1,28 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +/** + * @author Christoph Strobl + * @since 4.1 + */ +public interface Encryption { + + T encrypt(S value, EncryptionOptions options); + + S decrypt(T value); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index bf7472201c..9c2d8f562a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -1,11 +1,11 @@ /* - * Copyright 2023. the original author or authors. + * Copyright 2023 the original author or authors. * * 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 + * https://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, @@ -26,14 +26,6 @@ */ public interface EncryptionContext extends ValueConversionContext { - default String getAlgorithm() { - Encrypted annotation = lookupEncryptedAnnotation(); - if (annotation == null) { - throw new IllegalStateException("No algorithm defined for property " + getProperty().getName()); - } - return annotation.algorithm(); - } - default boolean isExplicitlyEncrypted() { return getProperty().isAnnotationPresent(ExplicitlyEncrypted.class); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java index 3fec05c3ff..7e57695923 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java @@ -1,11 +1,11 @@ /* - * Copyright 2023. the original author or authors. + * Copyright 2023 the original author or authors. * * 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 + * https://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, @@ -18,23 +18,47 @@ import org.bson.BsonBinary; /** + * The {@link EncryptionKey} represents a {@literal Data Encryption Key} reference that can be either direct via the + * {@link KeyId key id} or its {@link AltKeyName Key Alternative Name}. + * * @author Christoph Strobl * @since 4.1 */ public interface EncryptionKey { + /** + * @return the value that allows to reference a specific key + */ Object value(); + /** + * @return the {@link Type} of reference. + */ Type type(); + /** + * Create a new {@link EncryptionKey} that uses the keys id for reference. + * + * @param key must not be {@literal null}. + * @return new instance of {@link KeyId}. + */ static KeyId keyId(BsonBinary key) { return new KeyId(key); } + /** + * Create a new {@link EncryptionKey} that uses an {@literal Key Alternative Name} for reference. + * + * @param altKeyName must not be {@literal null}. + * @return new instance of {@link KeyId}. + */ static AltKeyName altKeyName(String altKeyName) { return new AltKeyName(altKeyName); } + /** + * @param value must not be {@literal null}. + */ record KeyId(BsonBinary value) implements EncryptionKey { @Override @@ -43,6 +67,9 @@ public Type type() { } } + /** + * @param value must not be {@literal null}. + */ record AltKeyName(String value) implements EncryptionKey { @Override @@ -51,7 +78,19 @@ public Type type() { } } + /** + * The key reference type. + */ enum Type { - ID, ALT + + /** + * Key referenced via its {@literal id}. + */ + ID, + + /** + * Key referenced via an {@literal Key Alternative Name}. + */ + ALT } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java deleted file mode 100644 index 6f86348b5e..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyProvider.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2023 the original author or authors. - * - * 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 - * - * https://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 org.springframework.data.mongodb.core.encryption; - -import java.util.function.Supplier; - -import org.springframework.util.StringUtils; - -/** - * @author Christoph Strobl - */ -public interface EncryptionKeyProvider { - - EncryptionKey getKey(EncryptionContext encryptionContext); - - static EncryptionKeyProvider annotationBasedKeyProvider() { - return annotationBasedKeyProvider(() -> { - throw new IllegalStateException("No Encryption key found"); - }); - } - - static EncryptionKeyProvider annotationBasedKeyProvider(Supplier fallback) { - - return ((encryptionContext) -> { - - if (encryptionContext.isExplicitlyEncrypted()) { - - ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); - if (StringUtils.hasText(annotation.altKeyName())) { - String altKeyName = annotation.altKeyName(); - if (altKeyName.startsWith("/")) { - String fieldName = altKeyName.replace("/", ""); - return new EncryptionKey.AltKeyName(encryptionContext.lookupValue(fieldName).toString()); - } else { - return new EncryptionKey.AltKeyName(altKeyName); - } - } - } - // if (encryptionContext.getKeyId() != null && !ObjectUtils.isEmpty(encryptionContext.getKeyId())) { - // // TODO: resolve the hash - // // return EncryptionKey.keyId() - // throw new IllegalStateException(); - // } - return fallback.get(); - }); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java new file mode 100644 index 0000000000..ef159d14b3 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java @@ -0,0 +1,56 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import java.util.function.Function; + +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +public interface EncryptionKeyResolver { + + EncryptionKey getKey(EncryptionContext encryptionContext); + + static EncryptionKeyResolver annotationBased() { + return annotationBased((ctx) -> { + throw new IllegalStateException("No Encryption key found"); + }); + } + + static EncryptionKeyResolver annotationBased(Function fallback) { + + return ((encryptionContext) -> { + + ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); + if (annotation == null || !StringUtils.hasText(annotation.altKeyName())) { + return fallback.apply(encryptionContext); + } + + String altKeyName = annotation.altKeyName(); + if (altKeyName.startsWith("/")) { + Object fieldValue = encryptionContext.lookupValue(altKeyName.replace("/", "")); + if (fieldValue == null) { + throw new IllegalStateException(String.format("Key Alternative Name for %s was null", altKeyName)); + } + return new EncryptionKey.AltKeyName(fieldValue.toString()); + } else { + return new EncryptionKey.AltKeyName(altKeyName); + } + }); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java new file mode 100644 index 0000000000..83a5d2a258 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -0,0 +1,44 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 4.1 + */ +public class EncryptionOptions { + + private final String algorithm; + private @Nullable EncryptionKey key; + + public EncryptionOptions(String algorithm) { + this.algorithm = algorithm; + } + + public void setKey(EncryptionKey key) { + this.key = key; + } + + public EncryptionKey key() { + return key; + } + + public String algorithm() { + return algorithm; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java similarity index 66% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java index e7513790ee..187aad7d51 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ManualEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java @@ -15,29 +15,20 @@ */ package org.springframework.data.mongodb.core.encryption; -import org.bson.BsonBinary; import org.springframework.data.mongodb.core.convert.MongoConversionContext; -import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.data.util.Lazy; import org.springframework.lang.Nullable; /** * @author Christoph Strobl * @since 4.1 */ -public class ManualEncryptionContext implements EncryptionContext { +class ExplicitEncryptionContext implements EncryptionContext { private final MongoConversionContext conversionContext; - private final MongoPersistentProperty persistentProperty; - private final EncryptionKeyProvider encryptionKeyProvider; - private final Lazy encryption; - public ManualEncryptionContext(MongoConversionContext conversionContext, EncryptionKeyProvider encryptionKeyProvider) { + public ExplicitEncryptionContext(MongoConversionContext conversionContext) { this.conversionContext = conversionContext; - this.persistentProperty = conversionContext.getProperty(); - this.encryption = Lazy.of(() -> persistentProperty.findAnnotation(Encrypted.class)); - this.encryptionKeyProvider = encryptionKeyProvider; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java index 67b1978f3d..4fb1c712dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java @@ -26,8 +26,28 @@ import org.springframework.data.mongodb.core.mapping.Encrypted; /** + * {@link ExplicitlyEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that + * indicates the target element is subject to encryption during the mapping process, in which a given domain type is + * converted into the store specific format. + *

+ * The {@link #value()} attribute, defines the bean type to look up within the + * {@link org.springframework.context.ApplicationContext} to obtain the {@link EncryptingConverter} responsible for the + * actual {@literal en-/decryption} while {@link #algorithm()} and {@link #altKeyName()} can be used to define aspects + * of the encryption process. + * + *

+ * public class Patient {
+ * 	private ObjectId id;
+ * 	private String name;
+ *
+ * 	@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "secred-key-alternative-name") //
+ * 	private String ssn;
+ * }
+ * 
+ * * @author Christoph Strobl * @since 4.1 + * @see ValueConverter */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @@ -35,11 +55,38 @@ @ValueConverter public @interface ExplicitlyEncrypted { + /** + * Define the algorithm to use. + *

+ * A {@literal Deterministic} algorithm ensures that a given input value always encrypts to the same output while a + * {@literal randomized} one will produce different results every time. + *

+ * Please make sure to use an algorithm that is in line with MongoDB's encryption rules for simple types, complex + * objects and arrays as well as the query limitations that come with each of them. + * + * @return the string representation of the encryption algorithm to use. + */ @AliasFor(annotation = Encrypted.class, value = "algorithm") String algorithm() default ""; + /** + * Set the {@literal Key Alternate Name} that references the {@literal Data Encryption Key} to be used. + *

+ * An empty String indicates that no alternative key name was configured. + *

+ * It is possible to use the {@literal "/"} character as a prefix to access a particular field value in the same + * domain type. In this case {@code "/name"} references the value of the {@literal name} field. Please note that + * update operations will require the full object to resolve those values. + * + * @return the {@literal Key Alternate Name} if set or an empty {@link String}. + */ String altKeyName() default ""; + /** + * The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property. + * + * @return the configured {@link EncryptingConverter}. A {@link ClientEncryptionConverter} by default. + */ @AliasFor(annotation = ValueConverter.class, value = "value") Class value() default ClientEncryptionConverter.class; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java new file mode 100644 index 0000000000..7085f76170 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java @@ -0,0 +1,80 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; + +import org.bson.BsonBinary; +import org.bson.BsonValue; +import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; + +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; + +/** + * @author Christoph Strobl + * @since 4.1 + */ +public class MongoClientEncryption implements Encryption { + + private final Supplier source; + private final AtomicReference cached; + + private MongoClientEncryption(Supplier source) { + this.source = source; + this.cached = new AtomicReference<>(source.get()); + } + + public static MongoClientEncryption caching(Supplier clientEncryption) { + return new MongoClientEncryption(clientEncryption); + } + + public static MongoClientEncryption just(ClientEncryption clientEncryption) { + return new MongoClientEncryption(() -> clientEncryption); + } + + public boolean refresh() { + cached.set(source.get()); + return true; + } + + void shutdown() { + getClientEncryption().close(); + } + + public BsonValue decrypt(BsonBinary value) { + return getClientEncryption().decrypt(value); + } + + public BsonBinary encrypt(BsonValue value, EncryptionOptions options) { + + EncryptOptions encryptOptions = new EncryptOptions(options.algorithm()); + + if (Type.ALT.equals(options.key().type())) { + encryptOptions = encryptOptions.keyAltName(options.key().value().toString()); + } else { + encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value()); + } + + return getClientEncryption().encrypt(value, encryptOptions); + } + + public ClientEncryption getClientEncryption() { + return cached.get(); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index bfa14c65a3..b7df4c8ec3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -48,9 +48,9 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.encryption.ClientEncryptionConverter; -import org.springframework.data.mongodb.core.encryption.ClientEncryptionProvider; +import org.springframework.data.mongodb.core.encryption.MongoClientEncryption; import org.springframework.data.mongodb.core.encryption.EncryptionKey; -import org.springframework.data.mongodb.core.encryption.EncryptionKeyProvider; +import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; import org.springframework.data.mongodb.core.encryption.ExplicitlyEncrypted; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.fle.FLETests.Config; @@ -178,12 +178,12 @@ void aggregationWithMatch() { } @Test - void altKeyDetection(@Autowired ClientEncryptionProvider clientEncryptionProvider) throws InterruptedException { + void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) throws InterruptedException { - BsonBinary user1key = clientEncryptionProvider.getClientEncryption().createDataKey("local", + BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("user-1"))); - BsonBinary user2key = clientEncryptionProvider.getClientEncryption().createDataKey("local", + BsonBinary user2key = mongoClientEncryption.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("user-2"))); Person p1 = new Person(); @@ -214,13 +214,13 @@ void altKeyDetection(@Autowired ClientEncryptionProvider clientEncryptionProvide // System.out.println(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()); // System.out.println(template.query(Person.class).matching(where("id").is(p2.id)).firstValue()); - DeleteResult deleteResult = clientEncryptionProvider.getClientEncryption().deleteKey(user2key); - clientEncryptionProvider.getClientEncryption().getKeys().forEach(System.out::println); + DeleteResult deleteResult = mongoClientEncryption.getClientEncryption().deleteKey(user2key); + mongoClientEncryption.getClientEncryption().getKeys().forEach(System.out::println); System.out.println("deleteResult: " + deleteResult); // System.out.println("---- waiting for cache timeout ----"); // TimeUnit.SECONDS.sleep(90); - clientEncryptionProvider.refresh(); + mongoClientEncryption.refresh(); assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1); @@ -251,23 +251,34 @@ protected void configureConverters(MongoConverterConfigurationAdapter converterC } @Bean - ClientEncryptionConverter encryptingConverter(ClientEncryptionProvider clientEncryptionProvider) { + ClientEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) { - Lazy dataKey = Lazy.of(() -> clientEncryptionProvider.getClientEncryption().createDataKey("local", + Lazy dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); - return new ClientEncryptionConverter(clientEncryptionProvider, - EncryptionKeyProvider.annotationBasedKeyProvider(() -> EncryptionKey.keyId(dataKey.get()))); + return new ClientEncryptionConverter(mongoClientEncryption, + EncryptionKeyResolver.annotationBased((ctx) -> EncryptionKey.keyId(dataKey.get()))); } @Bean - ClientEncryptionProvider clientEncryption(ClientEncryptionSettings encryptionSettings) { - return ClientEncryptionProvider.caching(() -> ClientEncryptions.create(encryptionSettings)); + MongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { + return MongoClientEncryption.caching(() -> ClientEncryptions.create(encryptionSettings)); } @Bean ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) { + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames"))); + + MongoCollection collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test"); + collection.drop(); // Clear old data + final byte[] localMasterKey = new byte[96]; new SecureRandom().nextBytes(localMasterKey); Map> kmsProviders = new HashMap>() { @@ -280,17 +291,6 @@ ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) { } }; - MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); - MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) - .getCollection(keyVaultNamespace.getCollectionName()); - keyVaultCollection.drop(); - // Ensure that two data keys cannot share the same keyAltName. - keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), - new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames"))); - - MongoCollection collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test"); - collection.drop(); // Clear old data - // Create the ClientEncryption instance ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() .keyVaultMongoClientSettings( From 6b8885425a823607dd7a057e23554abd5b161f93 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 15 Feb 2023 08:57:08 +0100 Subject: [PATCH 16/24] Some polishing --- .../encryption/ClientEncryptionConverter.java | 51 +++++++++---------- .../core/encryption/EncryptingConverter.java | 21 ++++++++ .../mongodb/core/encryption/Encryption.java | 15 ++++++ .../core/encryption/EncryptionContext.java | 33 ++++++++++-- .../encryption/EncryptionKeyResolver.java | 34 +++++++++---- .../core/encryption/EncryptionOptions.java | 2 + .../encryption/ExplicitEncryptionContext.java | 4 +- .../encryption/MongoClientEncryption.java | 37 +++++++++++++- 8 files changed, 153 insertions(+), 44 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java index b5768c4ac4..0cfe2ebd52 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java @@ -87,7 +87,7 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } else { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); iterable.forEach(it -> { - collection.add(context.getSourceContext().read(BsonUtils.toJavaType((BsonValue) it), + collection.add(context.getValueConversionContext().read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); }); return collection; @@ -99,7 +99,7 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) { - return context.getSourceContext().read(BsonUtils.toJavaType(bsonDocument), + return context.getValueConversionContext().read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation()); } @@ -131,7 +131,7 @@ public Object encrypt(Object value, EncryptionContext context) { encryptionOptions); } - Object write = context.getSourceContext().write(value); + Object write = context.getValueConversionContext().write(value); if (write instanceof Document doc) { return encryptionProvider.encrypt(doc.toBsonDocument(), encryptionOptions); } @@ -141,35 +141,30 @@ public Object encrypt(Object value, EncryptionContext context) { public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, EncryptionContext context) { - if (property.isCollectionLike()) { - - BsonArray bsonArray = new BsonArray(); - if (!property.isEntity()) { - if (value instanceof Collection values) { - values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - bsonArray.add(BsonUtils.simpleToBsonValue(o)); - } + BsonArray bsonArray = new BsonArray(); + if (!property.isEntity()) { + if (value instanceof Collection values) { + values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + bsonArray.add(BsonUtils.simpleToBsonValue(o)); } - return bsonArray; - } else { - if (value instanceof Collection values) { - values.forEach(it -> { - Document write = (Document) context.getSourceContext().write(it, property.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); - }); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) context.getSourceContext().write(o, property.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); - } + } + return bsonArray; + } else { + if (value instanceof Collection values) { + values.forEach(it -> { + Document write = (Document) context.getValueConversionContext().write(it, property.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); + }); + } else if (ObjectUtils.isArray(value)) { + for (Object o : ObjectUtils.toObjectArray(value)) { + Document write = (Document) context.getValueConversionContext().write(o, property.getTypeInformation()); + bsonArray.add(write.toBsonDocument()); } - return bsonArray; } + return bsonArray; } - - return null; } public EncryptionContext buildEncryptionContext(MongoConversionContext context) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java index 3067366b00..3d3df55bbe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java @@ -36,9 +36,30 @@ default T write(Object value, MongoConversionContext context) { return encrypt(value, buildEncryptionContext(context)); } + /** + * Decrypt the given encrypted source value within the given {@link EncryptionContext context}. + * + * @param encryptedValue the encrypted source. + * @param context the context to operate in. + * @return never {@literal null}. + */ S decrypt(Object encryptedValue, EncryptionContext context); + /** + * Encrypt the given raw source value within the given {@link EncryptionContext context}. + * + * @param value the encrypted source. + * @param context the context to operate in. + * @return never {@literal null}. + */ T encrypt(Object value, EncryptionContext context); + /** + * Obtain the {@link EncryptionContext} for a given {@link MongoConversionContext value conversion context}. + * + * @param context the current MongoDB specific {@link org.springframework.data.convert.ValueConversionContext}. + * @return the {@link EncryptionContext} to operate in. + * @see org.springframework.data.convert.ValueConversionContext + */ EncryptionContext buildEncryptionContext(MongoConversionContext context); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java index 717b9d7e00..70897d3cb7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java @@ -16,13 +16,28 @@ package org.springframework.data.mongodb.core.encryption; /** + * Component responsible for en-/decrypting values. + * * @author Christoph Strobl * @since 4.1 */ public interface Encryption { + /** + * Encrypt the given value. + * + * @param value must not be {@literal null}. + * @param options must not be {@literal null}. + * @return the encrypted value. + */ T encrypt(S value, EncryptionOptions options); + /** + * Decrypt the given value. + * + * @param value must not be {@literal null}. + * @return the decrypted value. + */ S decrypt(T value); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 9c2d8f562a..567daef35a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -26,19 +26,43 @@ */ public interface EncryptionContext extends ValueConversionContext { + /** + * @return {@literal true} if the {@link ExplicitlyEncrypted} annotation is present. + */ default boolean isExplicitlyEncrypted() { return getProperty().isAnnotationPresent(ExplicitlyEncrypted.class); } + /** + * Lookup the value for a given path within the current context. + * + * @param path the path/property name to resolve the current value for. + * @return can be {@literal null}. + */ @Nullable default Object lookupValue(String path) { - return getSourceContext().getValue(path); + return getValueConversionContext().getValue(path); } + /** + * Shortcut for converting a given {@literal value} into its store representation using the root + * {@link ValueConversionContext}. + * + * @param value + * @return + */ default Object convertToMongoType(Object value) { - return getSourceContext().write(value); + return getValueConversionContext().write(value); } + /** + * Search for the {@link Encrypted} annotation on both the {@link org.springframework.data.mapping.PersistentProperty + * property} as well as the {@link org.springframework.data.mapping.PersistentEntity entity} and return the first + * found + * + * @return can be {@literal null}. + */ + @Nullable default Encrypted lookupEncryptedAnnotation() { // TODO: having the path present here would really be helpful to inherit the algorithm @@ -46,5 +70,8 @@ default Encrypted lookupEncryptedAnnotation() { return annotation != null ? annotation : getProperty().getOwner().findAnnotation(Encrypted.class); } - MongoConversionContext getSourceContext(); + /** + * @return the {@link ValueConversionContext}. + */ + MongoConversionContext getValueConversionContext(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java index ef159d14b3..d3b3518805 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java @@ -15,30 +15,44 @@ */ package org.springframework.data.mongodb.core.encryption; -import java.util.function.Function; - import org.springframework.util.StringUtils; /** + * Interface to obtain a {@link EncryptionKey Data Encryption Key} that is valid in a given {@link EncryptionContext + * context}. + *

+ * Use the {@link #annotationBased(EncryptionKeyResolver) based} variant which will first try to resolve a potential + * {@link ExplicitlyEncrypted#altKeyName() Key Alternate Name} from annotations before calling the fallback resolver. + * * @author Christoph Strobl + * @since 4.1 + * @see EncryptionKey */ +@FunctionalInterface public interface EncryptionKeyResolver { + /** + * Get the {@link EncryptionKey Data Encryption Key}. + * + * @param encryptionContext the current {@link EncryptionContext context}. + * @return never {@literal null}. + */ EncryptionKey getKey(EncryptionContext encryptionContext); - static EncryptionKeyResolver annotationBased() { - return annotationBased((ctx) -> { - throw new IllegalStateException("No Encryption key found"); - }); - } - - static EncryptionKeyResolver annotationBased(Function fallback) { + /** + * Obtain an {@link EncryptionKeyResolver} that evaluates {@link ExplicitlyEncrypted#altKeyName()} and only calls the + * fallback {@link EncryptionKeyResolver resolver} if no {@literal Key Alternate Name} is present. + * + * @param fallback must not be {@literal null}. + * @return new instance of {@link EncryptionKeyResolver}. + */ + static EncryptionKeyResolver annotationBased(EncryptionKeyResolver fallback) { return ((encryptionContext) -> { ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); if (annotation == null || !StringUtils.hasText(annotation.altKeyName())) { - return fallback.apply(encryptionContext); + return fallback.getKey(encryptionContext); } String altKeyName = annotation.altKeyName(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index 83a5d2a258..3c2ba82a4a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -18,6 +18,8 @@ import org.springframework.lang.Nullable; /** + * Options, like the {@link #algorithm()}, to apply when encrypting values. + * * @author Christoph Strobl * @since 4.1 */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java index 187aad7d51..7d1e6b4dec 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java @@ -20,6 +20,8 @@ import org.springframework.lang.Nullable; /** + * Default {@link EncryptionContext} implementation. + * * @author Christoph Strobl * @since 4.1 */ @@ -43,7 +45,7 @@ public Object lookupValue(String path) { } @Override - public MongoConversionContext getSourceContext() { + public MongoConversionContext getValueConversionContext() { return conversionContext; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java index 7085f76170..6f1f5a2bd0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java @@ -21,11 +21,14 @@ import org.bson.BsonBinary; import org.bson.BsonValue; import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; +import org.springframework.util.Assert; import com.mongodb.client.model.vault.EncryptOptions; import com.mongodb.client.vault.ClientEncryption; /** + * {@link ClientEncryption} based {@link Encryption} implementation. + * * @author Christoph Strobl * @since 4.1 */ @@ -35,31 +38,61 @@ public class MongoClientEncryption implements Encryption private final AtomicReference cached; private MongoClientEncryption(Supplier source) { + this.source = source; this.cached = new AtomicReference<>(source.get()); } + /** + * The caching {@link MongoClientEncryption} variant caches and reuses the {@link ClientEncryption} obtained from the + * {@link Supplier} until explicitly {@link #refresh() refreshed}. + * + * @param clientEncryption must not be {@literal null} nor emit {@literal null}. + * @return new instance of {@link MongoClientEncryption}. + */ public static MongoClientEncryption caching(Supplier clientEncryption) { return new MongoClientEncryption(clientEncryption); } + /** + * Create a new {@link MongoClientEncryption} instance for the given {@link ClientEncryption}. + * + * @param clientEncryption must not be {@literal null}. + * @return new instance of {@link MongoClientEncryption}. + */ public static MongoClientEncryption just(ClientEncryption clientEncryption) { - return new MongoClientEncryption(() -> clientEncryption); + + Assert.notNull(clientEncryption, "ClientEncryption must not be null"); + + return new MongoClientEncryption(() -> clientEncryption) { + @Override + public boolean refresh() { + return false; + } + }; } + /** + * @return {@literal true} if refreshed, {@literal false} otherwise. + */ public boolean refresh() { cached.set(source.get()); return true; } - void shutdown() { + /** + * {@link ClientEncryption#close() Shutdown} the underlying {@link ClientEncryption}. + */ + public void shutdown() { getClientEncryption().close(); } + @Override public BsonValue decrypt(BsonBinary value) { return getClientEncryption().decrypt(value); } + @Override public BsonBinary encrypt(BsonValue value, EncryptionOptions options) { EncryptOptions encryptOptions = new EncryptOptions(options.algorithm()); From 6008824944741371bebe0eb6af67799798eed4e3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 15 Feb 2023 09:31:45 +0100 Subject: [PATCH 17/24] Add tests for MongoClientEncryption implementation --- .../core/encryption/EncryptionOptions.java | 4 +- .../MongoClientEncryptionUnitTests.java | 96 +++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index 3c2ba82a4a..de21acc2eb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -32,8 +32,10 @@ public EncryptionOptions(String algorithm) { this.algorithm = algorithm; } - public void setKey(EncryptionKey key) { + public EncryptionOptions setKey(EncryptionKey key) { + this.key = key; + return this; } public EncryptionKey key() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java new file mode 100644 index 0000000000..4b087ef42e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; + +import java.util.function.Supplier; + +import org.bson.BsonBinary; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.vault.ClientEncryption; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MockitoExtension.class) +class MongoClientEncryptionUnitTests { + + @Mock // + ClientEncryption clientEncryption; + + @Test // GH-4284 + void delegatesDecrypt() { + + MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption); + mce.decrypt(new BsonBinary(new byte[0])); + + verify(clientEncryption).decrypt(Mockito.any()); + } + + @Test // GH-4284 + void delegatesEncrypt() { + + MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption); + mce.encrypt(new BsonBinary(new byte[0]), + new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name"))); + + ArgumentCaptor options = ArgumentCaptor.forClass(EncryptOptions.class); + verify(clientEncryption).encrypt(any(), options.capture()); + assertThat(options.getValue().getAlgorithm()).isEqualTo(AEAD_AES_256_CBC_HMAC_SHA_512_Random); + assertThat(options.getValue().getKeyAltName()).isEqualTo("sec-key-name"); + } + + @Test // GH-4284 + void refreshHasNoEffectForFixedClientEncryption() { + + MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption); + mce.decrypt(new BsonBinary(new byte[0])); + + assertThat(mce.getClientEncryption()).isSameAs(clientEncryption); + assertThat(mce.refresh()).isFalse(); + assertThat(mce.getClientEncryption()).isSameAs(clientEncryption); + } + + @Test // GH-4284 + void refreshObtainsNextInstanceFromSupplier() { + + ClientEncryption next = mock(ClientEncryption.class); + + MongoClientEncryption mce = MongoClientEncryption.caching(new Supplier<>() { + + int counter = 0; + + @Override + public ClientEncryption get() { + return counter++ % 2 == 0 ? clientEncryption : next; + } + }); + + assertThat(mce.getClientEncryption()).isSameAs(clientEncryption); + assertThat(mce.refresh()).isTrue(); + assertThat(mce.getClientEncryption()).isSameAs(next); + } +} From 8e1f65737be992ead1604a0be5403963cb6c6e65 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 15 Feb 2023 14:45:04 +0100 Subject: [PATCH 18/24] Add mooar tests --- .../core/encryption/EncryptionKey.java | 39 +++++ .../core/encryption/ExplicitlyEncrypted.java | 4 +- ...ter.java => MongoEncryptionConverter.java} | 32 ++-- .../EncryptionKeyResolverUnitTests.java | 153 ++++++++++++++++++ .../MongoEncryptionConverterUnitTests.java | 74 +++++++++ .../data/mongodb/fle/FLETests.java | 6 +- .../test/util/MongoTestMappingContext.java | 13 ++ 7 files changed, 299 insertions(+), 22 deletions(-) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/{ClientEncryptionConverter.java => MongoEncryptionConverter.java} (81%) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java index 7e57695923..3235c46b45 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.encryption; import org.bson.BsonBinary; +import org.springframework.util.ObjectUtils; /** * The {@link EncryptionKey} represents a {@literal Data Encryption Key} reference that can be either direct via the @@ -65,6 +66,25 @@ record KeyId(BsonBinary value) implements EncryptionKey { public Type type() { return Type.ID; } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + KeyId that = (KeyId) o; + return ObjectUtils.nullSafeEquals(value, that.value); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(value); + } } /** @@ -76,6 +96,25 @@ record AltKeyName(String value) implements EncryptionKey { public Type type() { return Type.ALT; } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + AltKeyName that = (AltKeyName) o; + return ObjectUtils.nullSafeEquals(value, that.value); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(value); + } } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java index 4fb1c712dc..5e208d6af8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java @@ -85,8 +85,8 @@ /** * The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property. * - * @return the configured {@link EncryptingConverter}. A {@link ClientEncryptionConverter} by default. + * @return the configured {@link EncryptingConverter}. A {@link MongoEncryptionConverter} by default. */ @AliasFor(annotation = ValueConverter.class, value = "value") - Class value() default ClientEncryptionConverter.class; + Class value() default MongoEncryptionConverter.class; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java similarity index 81% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java index 0cfe2ebd52..ccf80cbecf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ClientEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java @@ -36,18 +36,17 @@ * @author Christoph Strobl * @since 4.1 */ -public class ClientEncryptionConverter implements EncryptingConverter { +public class MongoEncryptionConverter implements EncryptingConverter { - private static final Log LOGGER = LogFactory.getLog(ClientEncryptionConverter.class); + private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class); - private Encryption encryptionProvider; - private final EncryptionKeyResolver keyProvider; + private Encryption encryption; + private final EncryptionKeyResolver keyResolver; - public ClientEncryptionConverter(Encryption encryptionProvider, - EncryptionKeyResolver keyProvider) { + public MongoEncryptionConverter(Encryption encryption, EncryptionKeyResolver keyResolver) { - this.encryptionProvider = encryptionProvider; - this.keyProvider = keyProvider; + this.encryption = encryption; + this.keyResolver = keyResolver; } @Nullable @@ -69,7 +68,8 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { context.getProperty().getName())); } - decryptedValue = encryptionProvider.decrypt((BsonBinary) BsonUtils.simpleToBsonValue(encryptedValue)); + decryptedValue = encryption.decrypt((BsonBinary) BsonUtils.simpleToBsonValue(encryptedValue)); + // in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active // https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java if (encryptedValue == decryptedValue) { @@ -116,26 +116,24 @@ public Object encrypt(Object value, EncryptionContext context) { MongoPersistentProperty persistentProperty = context.getProperty(); EncryptionOptions encryptionOptions = new EncryptionOptions(context.lookupEncryptedAnnotation().algorithm()); - encryptionOptions.setKey(keyProvider.getKey(context)); + encryptionOptions.setKey(keyResolver.getKey(context)); if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { - return encryptionProvider.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), - encryptionOptions); + return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } - return encryptionProvider.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions); + return encryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions); } if (persistentProperty.isCollectionLike()) { - return encryptionProvider.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), - encryptionOptions); + return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } Object write = context.getValueConversionContext().write(value); if (write instanceof Document doc) { - return encryptionProvider.encrypt(doc.toBsonDocument(), encryptionOptions); + return encryption.encrypt(doc.toBsonDocument(), encryptionOptions); } - return encryptionProvider.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions); + return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions); } public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java new file mode 100644 index 0000000000..d7f87dd169 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java @@ -0,0 +1,153 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; + +import lombok.Data; + +import java.nio.charset.StandardCharsets; +import java.util.function.Function; + +import org.bson.BsonBinary; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.data.mongodb.test.util.MongoTestMappingContext; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class EncryptionKeyResolverUnitTests { + + @Mock // + EncryptionKeyResolver fallbackKeyResolver; + + MongoTestMappingContext mappingContext = MongoTestMappingContext.newTestContext().init(); + + EncryptionKey defaultEncryptionKey = EncryptionKey + .keyId(new BsonBinary("super-secret".getBytes(StandardCharsets.UTF_8))); + + @BeforeEach + void beforeEach() { + when(fallbackKeyResolver.getKey(any())).thenReturn(defaultEncryptionKey); + } + + @Test // GH-4284 + void usesDefaultKeyIfNoAnnotationPresent() { + + EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, + AnnotatedWithExplicitlyEncrypted::getNotAnnotated); + + EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + + assertThat(key).isSameAs(defaultEncryptionKey); + } + + @Test // GH-4284 + void usesDefaultKeyIfAnnotatedValueIsEmpty() { + + EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, + AnnotatedWithExplicitlyEncrypted::getAlgorithm); + + EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + + assertThat(key).isSameAs(defaultEncryptionKey); + } + + @Test // GH-4284 + void usesDefaultAltKeyNameIfPresent() { + + EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, + AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyName); + + EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + + assertThat(key).isEqualTo(EncryptionKey.altKeyName("sec-key-name")); + } + + @Test // GH-4284 + void readsAltKeyNameFromContextIfReferencingPropertyValue() { + + EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, + AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyNameFromPropertyValue); + when(ctx.lookupValue(eq("notAnnotated"))).thenReturn("born-to-be-wild"); + + EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + + assertThat(key).isEqualTo(EncryptionKey.altKeyName("born-to-be-wild")); + } + + private EncryptionContext prepareEncryptionContext(Class type, Function property) { + + EncryptionContext encryptionContext = mock(EncryptionContext.class); + when(encryptionContext.getProperty()).thenReturn(mappingContext.getPersistentPropertyFor(type, property)); + return encryptionContext; + } + + @Data + class AnnotatedWithExplicitlyEncrypted { + + String notAnnotated; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + String algorithm; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") // + String algorithmAndAltKeyName; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") // + String algorithmAndAltKeyNameFromPropertyValue; + } + // + // @Data + // class AnnotatedWithExplicitlyEncrypted { + // + // String notAnnotated; + // + // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + // String algorithm; + // + // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") // + // String algorithmAndAltKeyName; + // + // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") // + // String algorithmAndAltKeyNameFromPropertyValue; + // } + // + // @Data + // @Encrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) + // class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType { + // + // @ExplicitlyEncrypted // + // String algorithmFromDomainType; + // + // @ExplicitlyEncrypted(altKeyName = "sec-key-name") // + // String algorithmFromDomainTypeAndAltKeyNameFromProperty; + // + // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + // String algorithmFromProperty; + // } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java new file mode 100644 index 0000000000..9400bc5d5a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import org.bson.BsonBinary; +import org.bson.BsonValue; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MockitoExtension.class) +class MongoEncryptionConverterUnitTests { + + @Mock // + Encryption encryption; + + @Mock // + EncryptionKeyResolver keyResolver; + + MongoEncryptionConverter converter; + + @BeforeEach + void beforeEach() { + converter = new MongoEncryptionConverter(encryption, keyResolver); + } + + + + /* + static class Person { + + String id; + String name; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + String ssn; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // + String wallet; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random + Address address; + + AddressWithEncryptedZip encryptedZip; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + List listOfString; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + List

listOfComplex; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") // + String viaAltKeyNameField; + } + */ + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java index b7df4c8ec3..6bc631cad7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java @@ -47,7 +47,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; -import org.springframework.data.mongodb.core.encryption.ClientEncryptionConverter; +import org.springframework.data.mongodb.core.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.encryption.MongoClientEncryption; import org.springframework.data.mongodb.core.encryption.EncryptionKey; import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; @@ -251,12 +251,12 @@ protected void configureConverters(MongoConverterConfigurationAdapter converterC } @Bean - ClientEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) { + MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) { Lazy dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); - return new ClientEncryptionConverter(mongoClientEncryption, + return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver.annotationBased((ctx) -> EncryptionKey.keyId(dataKey.get()))); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestMappingContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestMappingContext.java index 4c0142ece5..11e6612ff4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestMappingContext.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestMappingContext.java @@ -16,10 +16,15 @@ package org.springframework.data.mongodb.test.util; import java.util.Collections; +import java.util.Optional; import java.util.function.Consumer; +import java.util.function.Function; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.util.MethodInvocationRecorder; /** * @author Christoph Strobl @@ -45,6 +50,12 @@ public MongoTestMappingContext(Consumer contextConfig) contextConfig.accept(contextConfigurer); } + public MongoPersistentProperty getPersistentPropertyFor(Class type, Function property) { + + MongoPersistentEntity persistentEntity = getRequiredPersistentEntity(type); + return persistentEntity.getPersistentProperty(MethodInvocationRecorder.forProxyOf(type).record(property).getPropertyPath().get()); + } + public MongoTestMappingContext customConversions(MongoConverterConfigurer converterConfig) { this.converterConfigurer = converterConfig; @@ -75,4 +86,6 @@ public MongoTestMappingContext init() { public void afterPropertiesSet() { init(); } + + } From 11f03f0df902c2a9cc1cd556ad7ab3cd385b6112 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 15 Feb 2023 15:52:31 +0100 Subject: [PATCH 19/24] pass on spel context to resolve expressions for key ids --- .../core/convert/MappingMongoConverter.java | 22 ++++---- .../core/convert/MongoConversionContext.java | 18 +++++++ .../core/encryption/EncryptionContext.java | 5 ++ .../encryption/EncryptionKeyResolver.java | 23 +++++++- .../util/encryption/EncryptionUtils.java | 3 +- .../EncryptionKeyResolverUnitTests.java | 53 +++++++++++++++++++ 6 files changed, 112 insertions(+), 12 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 9865c31d69..83f6db6406 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -331,7 +331,7 @@ private R doReadProjection(ConversionContext context, Bson bson, EntityProje PersistentPropertyAccessor convertingAccessor = PropertyTranslatingPropertyAccessor .create(new ConvertingPropertyAccessor<>(accessor, conversionService), propertyTranslator); MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, - evaluator); + evaluator, spELContext); readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator, Predicates.isTrue()); @@ -367,7 +367,7 @@ String getFieldName(MongoPersistentProperty prop) { populateProperties(context, mappedEntity, documentAccessor, evaluator, instance); PersistentPropertyAccessor convertingAccessor = new ConvertingPropertyAccessor<>(accessor, conversionService); - MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator); + MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator, spELContext); readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, evaluator, Predicates.isTrue()); @@ -529,7 +529,7 @@ private S populateProperties(ConversionContext context, MongoPersistentEntit ConversionContext contextToUse = context.withPath(currentPath); MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor, - evaluator); + evaluator, spELContext); Predicate propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate(); readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter); @@ -908,7 +908,7 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce public T getPropertyValue(MongoPersistentProperty property) { return (T) persistentPropertyAccessor.getProperty(property); } - }, prop, this))); + }, prop, this, spELContext))); return; } @@ -1251,7 +1251,7 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist public T getPropertyValue(MongoPersistentProperty property) { return (T) persistentPropertyAccessor.getProperty(property); } - }, property, this))); + }, property, this, spELContext))); return; } @@ -1857,6 +1857,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider T getPropertyValue(MongoPersistentProperty property) { CustomConversions conversions = context.getCustomConversions(); if (conversions.hasValueConverter(property)) { return (T) conversions.getPropertyValueConversions().getValueConverter(property).read(value, - new MongoConversionContext(this, property, context.getSourceConverter())); + new MongoConversionContext(this, property, context.getSourceConverter(), spELContext)); } ConversionContext contextToUse = context.forProperty(property); @@ -1914,7 +1916,7 @@ public T getPropertyValue(MongoPersistentProperty property) { public MongoDbPropertyValueProvider withContext(ConversionContext context) { - return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator); + return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator, spELContext); } } @@ -1937,7 +1939,7 @@ class AssociationAwareMongoDbPropertyValueProvider extends MongoDbPropertyValueP */ AssociationAwareMongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor source, SpELExpressionEvaluator evaluator) { - super(context, source, evaluator); + super(context, source, evaluator, MappingMongoConverter.this.spELContext); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index e3a67fbee8..30bd4c37df 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -15,12 +15,17 @@ */ package org.springframework.data.mongodb.core.convert; +import java.util.function.Supplier; + import org.bson.conversions.Bson; import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.model.PropertyValueProvider; +import org.springframework.data.mapping.model.SpELContext; +import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; +import org.springframework.expression.EvaluationContext; import org.springframework.lang.Nullable; /** @@ -35,11 +40,19 @@ public class MongoConversionContext implements ValueConversionContext accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { + this(accessor, persistentProperty, mongoConverter, null); + } + + public MongoConversionContext(PropertyValueProvider accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, SpELContext spELContext) { this.accessor = accessor; this.persistentProperty = persistentProperty; this.mongoConverter = mongoConverter; + this.spELContext = spELContext; } @Override @@ -61,4 +74,9 @@ public T read(@Nullable Object value, TypeInformation target) { return value instanceof Bson ? mongoConverter.read(target.getType(), (Bson) value) : ValueConversionContext.super.read(value, target); } + + @Nullable + public SpELContext getSpELContext() { + return spELContext; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 567daef35a..efe01dd072 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -19,6 +19,7 @@ import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.expression.EvaluationContext; import org.springframework.lang.Nullable; /** @@ -74,4 +75,8 @@ default Encrypted lookupEncryptedAnnotation() { * @return the {@link ValueConversionContext}. */ MongoConversionContext getValueConversionContext(); + + default EvaluationContext getEvaluationContext(Object source) { + return getValueConversionContext().getSpELContext().getEvaluationContext(source); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java index d3b3518805..df768a7317 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java @@ -15,6 +15,11 @@ */ package org.springframework.data.mongodb.core.encryption; +import org.bson.BsonBinary; +import org.bson.types.Binary; +import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.mongodb.util.encryption.EncryptionUtils; import org.springframework.util.StringUtils; /** @@ -52,7 +57,23 @@ static EncryptionKeyResolver annotationBased(EncryptionKeyResolver fallback) { ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); if (annotation == null || !StringUtils.hasText(annotation.altKeyName())) { - return fallback.getKey(encryptionContext); + + Encrypted encrypted = encryptionContext.getProperty().getOwner().findAnnotation(Encrypted.class); + if (encrypted == null) { + return fallback.getKey(encryptionContext); + } + + Object o = EncryptionUtils.resolveKeyId(encrypted.keyId()[0], + () -> encryptionContext.getEvaluationContext(new Object())); + if (o instanceof BsonBinary binary) { + return EncryptionKey.keyId(binary); + } + if (o instanceof Binary binary) { + return EncryptionKey.keyId((BsonBinary) BsonUtils.simpleToBsonValue(binary)); + } + if (o instanceof String string) { + return EncryptionKey.altKeyName(string); + } } String altKeyName = annotation.altKeyName(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java index 809d89a6a5..a0e51a106b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.util.encryption; +import java.util.Base64; import java.util.UUID; import java.util.function.Supplier; @@ -67,7 +68,7 @@ public static Object resolveKeyId(String value, Supplier eval new BsonBinary(UUID.fromString(potentialKeyId.toString())).getData()); } catch (IllegalArgumentException e) { - return new Binary(BsonBinarySubType.UUID_STANDARD, Base64Utils.decodeFromString(potentialKeyId.toString())); + return new Binary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode(potentialKeyId.toString())); } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java index d7f87dd169..a5944f6df2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java @@ -22,9 +22,11 @@ import lombok.Data; import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.function.Function; import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -32,7 +34,9 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.test.util.MongoTestMappingContext; +import org.springframework.expression.spel.support.StandardEvaluationContext; /** * @author Christoph Strobl @@ -99,6 +103,35 @@ void readsAltKeyNameFromContextIfReferencingPropertyValue() { assertThat(key).isEqualTo(EncryptionKey.altKeyName("born-to-be-wild")); } + @Test // GH-4284 + void readsKeyIdFromEncryptedAnnotationIfNoBetterCandidateAvailable() { + + EncryptionContext ctx = prepareEncryptionContext( + AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType.class, + AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType::getKeyIdFromDomainType); + + EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + + assertThat(key).isEqualTo(EncryptionKey.keyId( + new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g==")))); + } + + @Test // GH-4284 + void ignoresKeyIdFromEncryptedAnnotationWhenBetterCandidateAvailable() { + + EncryptionContext ctx = prepareEncryptionContext(KeyIdFromSpel.class, KeyIdFromSpel::getKeyIdFromDomainType); + + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + evaluationContext.setVariable("myKeyId", "xKVup8B1Q+CkHaVRx+qa+g=="); + + when(ctx.getEvaluationContext(any())).thenReturn(evaluationContext); + + EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + + assertThat(key).isEqualTo(EncryptionKey.keyId( + new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g==")))); + } + private EncryptionContext prepareEncryptionContext(Class type, Function property) { EncryptionContext encryptionContext = mock(EncryptionContext.class); @@ -120,6 +153,26 @@ class AnnotatedWithExplicitlyEncrypted { @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") // String algorithmAndAltKeyNameFromPropertyValue; } + + @Data + @Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==") + class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType { + + @ExplicitlyEncrypted // + String keyIdFromDomainType; + + @ExplicitlyEncrypted(altKeyName = "sec-key-name") // + String altKeyNameFromPropertyIgnoringKeyIdFromDomainType; + } + + @Data + @Encrypted(keyId = "#{#myKeyId}") + class KeyIdFromSpel { + + @ExplicitlyEncrypted // + String keyIdFromDomainType; + } + // // @Data // class AnnotatedWithExplicitlyEncrypted { From bb6865dcd29411104b5eb96d257c9e2f7c469728 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 22 Feb 2023 09:01:19 +0100 Subject: [PATCH 20/24] Additional tests --- .../core/encryption/EncryptionKey.java | 22 ++ .../core/encryption/EncryptionOptions.java | 32 ++ .../encryption/MongoEncryptionConverter.java | 13 +- .../EncryptionKeyResolverUnitTests.java | 31 -- .../encryption/EncryptionKeyUnitTests.java | 48 +++ .../encryption/EncryptionTests.java} | 353 ++++++++++++++---- .../MongoEncryptionConverterUnitTests.java | 220 ++++++++++- 7 files changed, 591 insertions(+), 128 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/{fle/FLETests.java => core/encryption/EncryptionTests.java} (52%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java index 3235c46b45..febaa762b6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.encryption; import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; import org.springframework.util.ObjectUtils; /** @@ -67,6 +68,18 @@ public Type type() { return Type.ID; } + @Override + public String toString() { + + if (BsonBinarySubType.isUuid(value.getType())) { + String representation = value.asUuid().toString(); + if (representation.length() > 6) { + return String.format("KeyId('%s***')", representation.substring(0, 6)); + } + } + return "KeyId('***')"; + } + @Override public boolean equals(Object o) { @@ -97,6 +110,15 @@ public Type type() { return Type.ALT; } + @Override + public String toString() { + + if (value().length() <= 3) { + return "AltKeyName('***')"; + } + return String.format("AltKeyName('%s***')", value.substring(0, 3)); + } + @Override public boolean equals(Object o) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index de21acc2eb..37805ecec2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.encryption; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; /** * Options, like the {@link #algorithm()}, to apply when encrypting values. @@ -45,4 +46,35 @@ public EncryptionKey key() { public String algorithm() { return algorithm; } + + @Override + public String toString() { + return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}'; + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + EncryptionOptions that = (EncryptionOptions) o; + + if (!ObjectUtils.nullSafeEquals(algorithm, that.algorithm)) { + return false; + } + return ObjectUtils.nullSafeEquals(key, that.key); + } + + @Override + public int hashCode() { + + int result = ObjectUtils.nullSafeHashCode(algorithm); + result = 31 * result + ObjectUtils.nullSafeHashCode(key); + return result; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java index ccf80cbecf..47cf611c91 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.encryption; import java.util.Collection; +import java.util.LinkedHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -95,6 +96,10 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } if (!persistentProperty.isEntity() && decryptedValue instanceof BsonValue bsonValue) { + if (persistentProperty.isMap() && persistentProperty.getType() != Document.class) { + return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue)); + + } return BsonUtils.toJavaType(bsonValue); } @@ -123,13 +128,19 @@ public Object encrypt(Object value, EncryptionContext context) { if (persistentProperty.isCollectionLike()) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } + if (persistentProperty.isMap()) { + Object convertedMap = context.getValueConversionContext().write(value, persistentProperty.getTypeInformation()); + if (convertedMap instanceof Document document) { + return encryption.encrypt(document.toBsonDocument(), encryptionOptions); + } + } return encryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions); } if (persistentProperty.isCollectionLike()) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } - Object write = context.getValueConversionContext().write(value); + Object write = context.getValueConversionContext().write(value, persistentProperty.getTypeInformation()); if (write instanceof Document doc) { return encryption.encrypt(doc.toBsonDocument(), encryptionOptions); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java index a5944f6df2..382d086d8e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java @@ -172,35 +172,4 @@ class KeyIdFromSpel { @ExplicitlyEncrypted // String keyIdFromDomainType; } - - // - // @Data - // class AnnotatedWithExplicitlyEncrypted { - // - // String notAnnotated; - // - // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // - // String algorithm; - // - // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") // - // String algorithmAndAltKeyName; - // - // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") // - // String algorithmAndAltKeyNameFromPropertyValue; - // } - // - // @Data - // @Encrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) - // class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType { - // - // @ExplicitlyEncrypted // - // String algorithmFromDomainType; - // - // @ExplicitlyEncrypted(altKeyName = "sec-key-name") // - // String algorithmFromDomainTypeAndAltKeyNameFromProperty; - // - // @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // - // String algorithmFromProperty; - // } - } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java new file mode 100644 index 0000000000..1098f4c2be --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import static org.assertj.core.api.Assertions.*; + +import java.util.UUID; + +import org.bson.BsonBinary; +import org.bson.UuidRepresentation; +import org.junit.jupiter.api.Test; + +/** + * @author Christoph Strobl + */ +class EncryptionKeyUnitTests { + + @Test // GH-4284 + void keyIdToStringDoesNotRevealEntireKey() { + + UUID uuid = UUID.randomUUID(); + + assertThat(EncryptionKey.keyId(new BsonBinary(uuid, UuidRepresentation.STANDARD)).toString()) + .contains(uuid.toString().substring(0, 6) + "***"); + } + + @Test // GH-4284 + void altKeyNameToStringDoesNotRevealEntireKey() { + + assertThat(EncryptionKey.altKeyName("s").toString()).contains("***"); + assertThat(EncryptionKey.altKeyName("su").toString()).contains("***"); + assertThat(EncryptionKey.altKeyName("sup").toString()).contains("***"); + assertThat(EncryptionKey.altKeyName("super-secret-key").toString()).contains("sup***"); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java similarity index 52% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java index 6bc631cad7..47283e061b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/fle/FLETests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2022 the original author or authors. + * Copyright 2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.fle; +package org.springframework.data.mongodb.core.encryption; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; @@ -30,7 +30,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import org.assertj.core.api.Assertions; import org.bson.BsonBinary; import org.bson.Document; import org.bson.types.Binary; @@ -47,13 +50,8 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; -import org.springframework.data.mongodb.core.encryption.MongoEncryptionConverter; -import org.springframework.data.mongodb.core.encryption.MongoClientEncryption; -import org.springframework.data.mongodb.core.encryption.EncryptionKey; -import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; -import org.springframework.data.mongodb.core.encryption.ExplicitlyEncrypted; +import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config; import org.springframework.data.mongodb.core.query.Update; -import org.springframework.data.mongodb.fle.FLETests.Config; import org.springframework.data.util.Lazy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -68,7 +66,6 @@ import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.Indexes; import com.mongodb.client.model.vault.DataKeyOptions; -import com.mongodb.client.result.DeleteResult; import com.mongodb.client.vault.ClientEncryptions; /** @@ -76,90 +73,225 @@ */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = Config.class) -public class FLETests { +public class EncryptionTests { @Autowired MongoTemplate template; - @Test - void manualEnAndDecryption() { + @Test // GH-4284 + void enDeCryptSimpleValue() { - Person person = new Person(); - person.id = "id-1"; - person.name = "p1-name"; - person.ssn = "mySecretSSN"; // determinisitc encryption (queryable) - person.wallet = "myEvenMoreSecretStuff"; // random encryption (non queryable) + Person source = new Person(); + source.id = "id-1"; + source.ssn = "mySecretSSN"; - // nested full document encryption - person.address = new Address(); - person.address.city = "NYC"; - person.address.street = "4th Ave."; + template.save(source); - person.encryptedZip = new AddressWithEncryptedZip(); - person.encryptedZip.city = "Boston"; - person.encryptedZip.street = "central square"; - person.encryptedZip.zip = "1234567890"; + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } - person.listOfString = Arrays.asList("spring", "data", "mongodb"); + @Test // GH-4284 + void enDeCryptComplexValue() { - Address partOfList = new Address(); - partOfList.city = "SFO"; - partOfList.street = "---"; - person.listOfComplex = Collections.singletonList(partOfList); + Person source = new Person(); + source.id = "id-1"; + source.address = new Address(); + source.address.city = "NYC"; + source.address.street = "4th Ave."; - template.save(person); + template.save(source); - System.out.println("source: " + person); + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } - Document savedDocument = template.execute(Person.class, collection -> { - return collection.find(new Document()).first(); - }); + @Test // GH-4284 + void enDeCryptValueWithinComplexOne() { + + Person source = new Person(); + source.id = "id-1"; + source.encryptedZip = new AddressWithEncryptedZip(); + source.encryptedZip.city = "Boston"; + source.encryptedZip.street = "central square"; + source.encryptedZip.zip = "1234567890"; + + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> { + assertThat(it.get("encryptedZip")).isInstanceOf(Document.class); + assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class); + assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class); + assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class); + }) // + .loadedIsEqualToSource(); + } + + @Test // GH-4284 + void enDeCryptListOfSimpleValue() { + + Person source = new Person(); + source.id = "id-1"; + source.listOfString = Arrays.asList("spring", "data", "mongodb"); - // ssn should look like "ssn": {"$binary": {"base64": "... - System.out.println("saved: " + savedDocument.toJson()); - assertThat(savedDocument.get("ssn")).isInstanceOf(Binary.class); - assertThat(savedDocument.get("wallet")).isInstanceOf(Binary.class); - assertThat(savedDocument.get("encryptedZip")).isInstanceOf(Document.class); - assertThat(savedDocument.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class); - assertThat(savedDocument.get("address")).isInstanceOf(Binary.class); - assertThat(savedDocument.get("listOfString")).isInstanceOf(Binary.class); - assertThat(savedDocument.get("listOfComplex")).isInstanceOf(Binary.class); - - // count should be 1 using a deterministic algorithm - long queryCount = template.query(Person.class).matching(where("ssn").is(person.ssn)).count(); - System.out.println("query(count): " + queryCount); - assertThat(queryCount).isOne(); - - Person bySsn = template.query(Person.class).matching(where("ssn").is(person.ssn)).firstValue(); - System.out.println("queryable: " + bySsn); - assertThat(bySsn).isEqualTo(person); - - Person byWallet = template.query(Person.class).matching(where("wallet").is(person.wallet)).firstValue(); - System.out.println("not-queryable: " + byWallet); - assertThat(byWallet).isNull(); + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); } - @Test - void theUpdateStuff() { + @Test // GH-4284 + void enDeCryptListOfComplexValue() { - Person person = new Person(); - person.id = "id-1"; - person.name = "p1-name"; + Person source = new Person(); + source.id = "id-1"; - template.save(person); + Address address = new Address(); + address.city = "SFO"; + address.street = "---"; - Document savedDocument = template.execute(Person.class, collection -> { - return collection.find(new Document()).first(); - }); - System.out.println("saved: " + savedDocument.toJson()); + source.listOfComplex = Collections.singletonList(address); + + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("listOfComplex")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } + + @Test // GH-4284 + void enDeCryptMapOfSimpleValues() { + + Person source = new Person(); + source.id = "id-1"; + source.mapOfString = Map.of("k1", "v1", "k2", "v2"); + + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("mapOfString")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } + + @Test // GH-4284 + void enDeCryptMapOfComplexValues() { + + Person source = new Person(); + source.id = "id-1"; + + Address address1 = new Address(); + address1.city = "SFO"; + address1.street = "---"; + + Address address2 = new Address(); + address2.city = "NYC"; + address2.street = "---"; + + source.mapOfComplex = Map.of("a1", address1, "a2", address2); + + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("mapOfComplex")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } + + @Test // GH-4284 + void canQueryDeterministicallyEncrypted() { + + Person source = new Person(); + source.id = "id-1"; + source.ssn = "mySecretSSN"; + + template.save(source); + + Person loaded = template.query(Person.class).matching(where("ssn").is(source.ssn)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4284 + void cannotQueryRandomlyEncrypted() { + + Person source = new Person(); + source.id = "id-1"; + source.wallet = "secret-wallet-id"; + + template.save(source); + + Person loaded = template.query(Person.class).matching(where("wallet").is(source.wallet)).firstValue(); + assertThat(loaded).isNull(); + } + + @Test // GH-4284 + void updateSimpleTypeEncryptedFieldWithNewValue() { + + Person source = new Person(); + source.id = "id-1"; - template.update(Person.class).matching(where("id").is(person.id)).apply(Update.update("ssn", "secret-value")) + template.save(source); + + template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value")) .first(); - savedDocument = template.execute(Person.class, collection -> { - return collection.find(new Document()).first(); - }); - System.out.println("updated: " + savedDocument.toJson()); - assertThat(savedDocument.get("ssn")).isInstanceOf(Binary.class); + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) // + .loadedMatches(it -> assertThat(it.getSsn()).isEqualTo("secret-value")); + } + + @Test // GH-4284 + void updateComplexTypeEncryptedFieldWithNewValue() { + + Person source = new Person(); + source.id = "id-1"; + + template.save(source); + + Address address = new Address(); + address.city = "SFO"; + address.street = "---"; + + template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("address", address)).first(); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) // + .loadedMatches(it -> assertThat(it.getAddress()).isEqualTo(address)); + } + + @Test // GH-4284 + void updateEncryptedFieldInNestedElementWithNewValue() { + + Person source = new Person(); + source.id = "id-1"; + source.encryptedZip = new AddressWithEncryptedZip(); + source.encryptedZip.city = "Boston"; + source.encryptedZip.street = "central square"; + + template.save(source); + + template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("encryptedZip.zip", "179")) + .first(); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> { + assertThat(it.get("encryptedZip")).isInstanceOf(Document.class); + assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class); + assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class); + assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class); + }) // + .loadedMatches(it -> assertThat(it.getEncryptedZip().getZip()).isEqualTo("179")); } @Test @@ -211,15 +343,10 @@ void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) thr return null; }); - // System.out.println(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()); - // System.out.println(template.query(Person.class).matching(where("id").is(p2.id)).firstValue()); - - DeleteResult deleteResult = mongoClientEncryption.getClientEncryption().deleteKey(user2key); - mongoClientEncryption.getClientEncryption().getKeys().forEach(System.out::println); - System.out.println("deleteResult: " + deleteResult); + // remove the key and invalidate encrypted data + mongoClientEncryption.getClientEncryption().deleteKey(user2key); - // System.out.println("---- waiting for cache timeout ----"); - // TimeUnit.SECONDS.sleep(90); + // clear the 60 second key cache within the mongo client mongoClientEncryption.refresh(); assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1); @@ -228,6 +355,64 @@ void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) thr .isThrownBy(() -> template.query(Person.class).matching(where("id").is(p2.id)).firstValue()); } + SaveAndLoadAssert verifyThat(T source) { + return new SaveAndLoadAssert<>(source); + } + + class SaveAndLoadAssert { + + T source; + Function idProvider; + + SaveAndLoadAssert(T source) { + this.source = source; + } + + SaveAndLoadAssert identifiedBy(Function idProvider) { + this.idProvider = idProvider; + return this; + } + + SaveAndLoadAssert wasSavedAs(Document expected) { + return wasSavedMatching(it -> Assertions.assertThat(it).isEqualTo(expected)); + } + + SaveAndLoadAssert wasSavedMatching(Consumer saved) { + EncryptionTests.this.assertSaved(source, idProvider, saved); + return this; + } + + SaveAndLoadAssert loadedMatches(Consumer expected) { + EncryptionTests.this.assertLoaded(source, idProvider, expected); + return this; + } + + SaveAndLoadAssert loadedIsEqualToSource() { + return loadedIsEqualTo(source); + } + + SaveAndLoadAssert loadedIsEqualTo(T expected) { + return loadedMatches(it -> Assertions.assertThat(it).isEqualTo(expected)); + } + + } + + void assertSaved(T source, Function idProvider, Consumer dbValue) { + + Document savedDocument = template.execute(Person.class, collection -> { + return collection.find(new Document("_id", idProvider.apply(source))).first(); + }); + dbValue.accept(savedDocument); + } + + void assertLoaded(T source, Function idProvider, Consumer loadedValue) { + + T loaded = template.query((Class) source.getClass()).matching(where("id").is(idProvider.apply(source))) + .firstValue(); + + loadedValue.accept(loaded); + } + @Configuration static class Config extends AbstractMongoClientConfiguration { @@ -326,6 +511,12 @@ static class Person { @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") // String viaAltKeyNameField; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + Map mapOfString; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + Map mapOfComplex; } @Data diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java index 9400bc5d5a..f84a0b3661 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java @@ -15,60 +15,250 @@ */ package org.springframework.data.mongodb.core.encryption; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; +import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; + +import lombok.Data; + +import java.util.List; +import java.util.Map; + +import org.bson.BsonArray; import org.bson.BsonBinary; +import org.bson.BsonString; import org.bson.BsonValue; +import org.bson.Document; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.test.util.MongoTestMappingContext; /** * @author Christoph Strobl */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) class MongoEncryptionConverterUnitTests { @Mock // Encryption encryption; @Mock // - EncryptionKeyResolver keyResolver; + EncryptionKeyResolver fallbackKeyResolver; + @Mock // + MongoConversionContext conversionContext; + + MongoTestMappingContext mappingContext = MongoTestMappingContext.newTestContext(); + EncryptionKeyResolver keyResolver; MongoEncryptionConverter converter; + @Captor ArgumentCaptor encryptionOptions; + + @Captor ArgumentCaptor valueToBeEncrypted; + @BeforeEach void beforeEach() { + + when(fallbackKeyResolver.getKey(any())).thenReturn(EncryptionKey.altKeyName("default")); + when(encryption.encrypt(valueToBeEncrypted.capture(), encryptionOptions.capture())) + .thenReturn(new BsonBinary(new byte[0])); + keyResolver = EncryptionKeyResolver.annotationBased(fallbackKeyResolver); converter = new MongoEncryptionConverter(encryption, keyResolver); } + @Test // GH-4284 + void delegatesConversionOfSimpleValueWithDefaultEncryptionKeyFromKeyResolver() { + + when(conversionContext.getProperty()) + .thenReturn(mappingContext.getPersistentPropertyFor(Type.class, Type::getStringValueWithAlgorithmOnly)); + + converter.write("foo", conversionContext); + + assertThat(valueToBeEncrypted.getValue()).isEqualTo(new BsonString("foo")); + assertThat(encryptionOptions.getValue()).isEqualTo( + new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic).setKey(EncryptionKey.altKeyName("default"))); + } + + @Test // GH-4284 + void favorsAltKeyNameIfPresent() { + + when(conversionContext.getProperty()).thenReturn( + mappingContext.getPersistentPropertyFor(Type.class, Type::getStringValueWithAlgorithmAndAltKeyName)); + + converter.write("foo", conversionContext); + + assertThat(encryptionOptions.getValue()).isEqualTo( + new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name"))); + } + + @Test // GH-4284 + void readsAltKeyNameFromProperty() { + + when(conversionContext.getProperty()).thenReturn(mappingContext.getPersistentPropertyFor(Type.class, + Type::getStringValueWithAlgorithmAndAltKeyNameFromPropertyValue)); + + ArgumentCaptor path = ArgumentCaptor.forClass(String.class); + when(conversionContext.getValue(path.capture())).thenReturn("(ツ)"); + + converter.write("foo", conversionContext); + assertThat(path.getValue()).isEqualTo("notAnnotated"); + + assertThat(encryptionOptions.getValue()) + .isEqualTo(new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("(ツ)"))); + } + + @Test // GH-4284 + void delegatesConversionOfEntityTypes() { + + Document convertedValue = new Document("unencryptedValue", "nested-unencrypted"); + MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, + Type::getNestedFullyEncrypted); + when(conversionContext.getProperty()).thenReturn(property); + doReturn(convertedValue).when(conversionContext).write(any(), eq(property.getTypeInformation())); + + ArgumentCaptor path = ArgumentCaptor.forClass(String.class); + when(conversionContext.getValue(path.capture())).thenReturn("(ツ)"); + + JustATypeWithAnUnencryptedField source = new JustATypeWithAnUnencryptedField(); + source.unencryptedValue = "nested-unencrypted"; + + converter.write(source, conversionContext); + + assertThat(valueToBeEncrypted.getValue()).isEqualTo(convertedValue.toBsonDocument()); + } + + @Test // GH-4284 + void listsOfSimpleTypesAreConvertedEntirely() { + + MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getListOfString); + when(conversionContext.getProperty()).thenReturn(property); + + converter.write(List.of("one", "two"), conversionContext); + + assertThat(valueToBeEncrypted.getValue()) + .isEqualTo(new BsonArray(List.of(new BsonString("one"), new BsonString("two")))); + } + + @Test // GH-4284 + void listsOfComplexTypesAreConvertedEntirely() { + + Document convertedValue1 = new Document("unencryptedValue", "nested-unencrypted-1"); + Document convertedValue2 = new Document("unencryptedValue", "nested-unencrypted-2"); + + MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getListOfComplex); + when(conversionContext.getProperty()).thenReturn(property); + doReturn(convertedValue1, convertedValue2).when(conversionContext).write(any(), eq(property.getTypeInformation())); + + JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField(); + source1.unencryptedValue = "nested-unencrypted-1"; + + JustATypeWithAnUnencryptedField source2 = new JustATypeWithAnUnencryptedField(); + source2.unencryptedValue = "nested-unencrypted-1"; + + converter.write(List.of(source1, source2), conversionContext); + + assertThat(valueToBeEncrypted.getValue()) + .isEqualTo(new BsonArray(List.of(convertedValue1.toBsonDocument(), convertedValue2.toBsonDocument()))); + } + + @Test // GH-4284 + void simpleMapsAreConvertedEntirely() { + + MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfString); + when(conversionContext.getProperty()).thenReturn(property); + doReturn(new Document("k1", "v1").append("k2", "v2")).when(conversionContext).write(any(), + eq(property.getTypeInformation())); + + converter.write(Map.of("k1", "v1", "k2", "v2"), conversionContext); + + assertThat(valueToBeEncrypted.getValue()) + .isEqualTo(new Document("k1", new BsonString("v1")).append("k2", new BsonString("v2")).toBsonDocument()); + } + + @Test // GH-4284 + void complexMapsAreConvertedEntirely() { + + Document convertedValue1 = new Document("unencryptedValue", "nested-unencrypted-1"); + Document convertedValue2 = new Document("unencryptedValue", "nested-unencrypted-2"); + + MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfComplex); + when(conversionContext.getProperty()).thenReturn(property); + doReturn(new Document("k1", convertedValue1).append("k2", convertedValue2)).when(conversionContext).write(any(), + eq(property.getTypeInformation())); + JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField(); + source1.unencryptedValue = "nested-unencrypted-1"; - /* - static class Person { + JustATypeWithAnUnencryptedField source2 = new JustATypeWithAnUnencryptedField(); + source2.unencryptedValue = "nested-unencrypted-1"; - String id; - String name; + converter.write(Map.of("k1", source1, "k2", source2), conversionContext); + + assertThat(valueToBeEncrypted.getValue()).isEqualTo(new Document("k1", convertedValue1.toBsonDocument()) + .append("k2", convertedValue2.toBsonDocument()).toBsonDocument()); + } + + @Data + static class Type { + + String notAnnotated; @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // - String ssn; + String stringValueWithAlgorithmOnly; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") // + String stringValueWithAlgorithmAndAltKeyName; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // - String wallet; + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") // + String stringValueWithAlgorithmAndAltKeyNameFromPropertyValue; @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random - Address address; + JustATypeWithAnUnencryptedField nestedFullyEncrypted; - AddressWithEncryptedZip encryptedZip; + NestedWithEncryptedField nestedWithEncryptedField; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + // Client-Side Field Level Encryption does not support encrypting individual array elements + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // List listOfString; + // Client-Side Field Level Encryption does not support encrypting individual array elements @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random - List
listOfComplex; + List listOfComplex; + + // just as it was a domain type encrypt the entire thing here + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + Map mapOfString; + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + Map mapOfComplex; + + RecordWithEncryptedValue recordWithEncryptedValue; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") // - String viaAltKeyNameField; + List listOfRecordWithEncryptedValue; } - */ + static class JustATypeWithAnUnencryptedField { + + String unencryptedValue; + } + + static class NestedWithEncryptedField extends JustATypeWithAnUnencryptedField { + + @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + String encryptedValue; + } + + record RecordWithEncryptedValue(@ExplicitlyEncrypted String value) { + } } From fd8ea42bee2f4d29a36d0c3f0d893df4eb3c082e Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 7 Mar 2023 14:00:33 +0100 Subject: [PATCH 21/24] Untangle EncryptionContext from ValueConversionContext --- .../encryption/EncryptingConverter.java | 3 +- .../encryption/ExplicitEncryptionContext.java | 24 ++++++- .../encryption/MongoEncryptionConverter.java | 47 ++++++++----- .../core/encryption/EncryptionContext.java | 67 +++++++++---------- .../encryption/EncryptionKeyResolver.java | 1 + .../ExplicitlyEncrypted.java | 5 +- .../EncryptionKeyResolverUnitTests.java | 1 + .../core/encryption/EncryptionTests.java | 2 + .../MongoEncryptionConverterUnitTests.java | 10 +-- 9 files changed, 98 insertions(+), 62 deletions(-) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/{ => convert}/encryption/EncryptingConverter.java (94%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/{ => convert}/encryption/ExplicitEncryptionContext.java (65%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/{ => convert}/encryption/MongoEncryptionConverter.java (74%) rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/{encryption => mapping}/ExplicitlyEncrypted.java (94%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java similarity index 94% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java index 3d3df55bbe..3bef1f280d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java @@ -13,10 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.core.encryption; +package org.springframework.data.mongodb.core.convert.encryption; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoValueConverter; +import org.springframework.data.mongodb.core.encryption.EncryptionContext; /** * A specialized {@link MongoValueConverter} for {@literal en-/decrypting} properties. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java similarity index 65% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java index 7d1e6b4dec..abe89977b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.core.encryption; +package org.springframework.data.mongodb.core.convert.encryption; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.encryption.EncryptionContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.expression.EvaluationContext; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; /** @@ -45,7 +48,22 @@ public Object lookupValue(String path) { } @Override - public MongoConversionContext getValueConversionContext() { - return conversionContext; + public Object convertToMongoType(Object value) { + return conversionContext.write(value); + } + + @Override + public EvaluationContext getEvaluationContext(Object source) { + return conversionContext.getSpELContext().getEvaluationContext(source); + } + + @Override + public T read(@Nullable Object value, @NonNull Class target) { + return conversionContext.read(value, target); + } + + @Override + public T write(@Nullable Object value, @NonNull Class target) { + return conversionContext.write(value, target); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java similarity index 74% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index 47cf611c91..e1418e6185 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.core.encryption; +package org.springframework.data.mongodb.core.convert.encryption; import java.util.Collection; import java.util.LinkedHashMap; @@ -28,6 +28,11 @@ import org.bson.types.Binary; import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.encryption.Encryption; +import org.springframework.data.mongodb.core.encryption.EncryptionContext; +import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; +import org.springframework.data.mongodb.core.encryption.EncryptionOptions; +import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; @@ -65,8 +70,8 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Decrypting %s.%s.", context.getProperty().getOwner().getName(), - context.getProperty().getName())); + LOGGER.debug(String.format("Decrypting %s.%s.", getProperty(context).getOwner().getName(), + getProperty(context).getName())); } decryptedValue = encryption.decrypt((BsonBinary) BsonUtils.simpleToBsonValue(encryptedValue)); @@ -78,9 +83,9 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } } - MongoPersistentProperty persistentProperty = context.getProperty(); + MongoPersistentProperty persistentProperty = getProperty(context); - if (context.getProperty().isCollectionLike() && decryptedValue instanceof Iterable iterable) { + if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable iterable) { if (!persistentProperty.isEntity()) { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); @@ -88,7 +93,7 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } else { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); iterable.forEach(it -> { - collection.add(context.getValueConversionContext().read(BsonUtils.toJavaType((BsonValue) it), + collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); }); return collection; @@ -104,23 +109,33 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) { - return context.getValueConversionContext().read(BsonUtils.toJavaType(bsonDocument), - persistentProperty.getTypeInformation()); + return context.read(BsonUtils.toJavaType(bsonDocument), + persistentProperty.getTypeInformation().getType()); } return decryptedValue; } + private MongoPersistentProperty getProperty(EncryptionContext context) { + return context.getProperty(); + } + @Override public Object encrypt(Object value, EncryptionContext context) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Encrypting %s.%s.", context.getProperty().getOwner().getName(), - context.getProperty().getName())); + LOGGER.debug(String.format("Encrypting %s.%s.", getProperty(context).getOwner().getName(), + getProperty(context).getName())); + } + + MongoPersistentProperty persistentProperty = getProperty(context); + + Encrypted annotation = persistentProperty.findAnnotation(Encrypted.class); + if(annotation == null) { + annotation = persistentProperty.getOwner().findAnnotation(Encrypted.class); } - MongoPersistentProperty persistentProperty = context.getProperty(); - EncryptionOptions encryptionOptions = new EncryptionOptions(context.lookupEncryptedAnnotation().algorithm()); + EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm()); encryptionOptions.setKey(keyResolver.getKey(context)); if (!persistentProperty.isEntity()) { @@ -129,7 +144,7 @@ public Object encrypt(Object value, EncryptionContext context) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } if (persistentProperty.isMap()) { - Object convertedMap = context.getValueConversionContext().write(value, persistentProperty.getTypeInformation()); + Object convertedMap = context.write(value, persistentProperty.getTypeInformation().getType()); if (convertedMap instanceof Document document) { return encryption.encrypt(document.toBsonDocument(), encryptionOptions); } @@ -140,7 +155,7 @@ public Object encrypt(Object value, EncryptionContext context) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } - Object write = context.getValueConversionContext().write(value, persistentProperty.getTypeInformation()); + Object write = context.write(value, persistentProperty.getTypeInformation().getType()); if (write instanceof Document doc) { return encryption.encrypt(doc.toBsonDocument(), encryptionOptions); } @@ -163,12 +178,12 @@ public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty } else { if (value instanceof Collection values) { values.forEach(it -> { - Document write = (Document) context.getValueConversionContext().write(it, property.getTypeInformation()); + Document write = (Document) context.write(it, property.getTypeInformation().getType()); bsonArray.add(write.toBsonDocument()); }); } else if (ObjectUtils.isArray(value)) { for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) context.getValueConversionContext().write(o, property.getTypeInformation()); + Document write = (Document) context.write(o, property.getTypeInformation().getType()); bsonArray.add(write.toBsonDocument()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index efe01dd072..a11a5166bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -16,67 +16,62 @@ package org.springframework.data.mongodb.core.encryption; import org.springframework.data.convert.ValueConversionContext; -import org.springframework.data.mongodb.core.convert.MongoConversionContext; -import org.springframework.data.mongodb.core.mapping.Encrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.expression.EvaluationContext; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; /** * @author Christoph Strobl */ -public interface EncryptionContext extends ValueConversionContext { +public interface EncryptionContext { /** - * @return {@literal true} if the {@link ExplicitlyEncrypted} annotation is present. - */ - default boolean isExplicitlyEncrypted() { - return getProperty().isAnnotationPresent(ExplicitlyEncrypted.class); - } - - /** - * Lookup the value for a given path within the current context. + * Returns the {@link MongoPersistentProperty} to be handled. * - * @param path the path/property name to resolve the current value for. - * @return can be {@literal null}. + * @return will never be {@literal null}. */ - @Nullable - default Object lookupValue(String path) { - return getValueConversionContext().getValue(path); - } + MongoPersistentProperty getProperty(); /** * Shortcut for converting a given {@literal value} into its store representation using the root * {@link ValueConversionContext}. - * + * * @param value * @return */ - default Object convertToMongoType(Object value) { - return getValueConversionContext().write(value); - } + Object convertToMongoType(Object value); /** - * Search for the {@link Encrypted} annotation on both the {@link org.springframework.data.mapping.PersistentProperty - * property} as well as the {@link org.springframework.data.mapping.PersistentEntity entity} and return the first - * found - * + * Reads the value as an instance of {@link Class type}. + * + * @param value {@link Object value} to be read; can be {@literal null}. + * @param target {@link Class type} of value to be read; must not be {@literal null}. * @return can be {@literal null}. + * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - @Nullable - default Encrypted lookupEncryptedAnnotation() { + T read(@Nullable Object value, @NonNull Class target); - // TODO: having the path present here would really be helpful to inherit the algorithm - Encrypted annotation = getProperty().findAnnotation(Encrypted.class); - return annotation != null ? annotation : getProperty().getOwner().findAnnotation(Encrypted.class); - } + /** + * Write the value as an instance of {@link Class type}. + * + * @param value {@link Object value} to write; can be {@literal null}. + * @param target {@link Class type} of value to be written; must not be {@literal null}. + * @return can be {@literal null}. + * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}. + */ + @Nullable + T write(@Nullable Object value, @NonNull Class target); /** - * @return the {@link ValueConversionContext}. + * Lookup the value for a given path within the current context. + * + * @param path the path/property name to resolve the current value for. + * @return can be {@literal null}. */ - MongoConversionContext getValueConversionContext(); + @Nullable + Object lookupValue(String path); + + EvaluationContext getEvaluationContext(Object source); - default EvaluationContext getEvaluationContext(Object source) { - return getValueConversionContext().getSpELContext().getEvaluationContext(source); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java index df768a7317..61deacff5e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java @@ -18,6 +18,7 @@ import org.bson.BsonBinary; import org.bson.types.Binary; import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.encryption.EncryptionUtils; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitlyEncrypted.java similarity index 94% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitlyEncrypted.java index 5e208d6af8..f64d1ed144 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/ExplicitlyEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitlyEncrypted.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.core.encryption; +package org.springframework.data.mongodb.core.mapping; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -23,7 +23,8 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.convert.ValueConverter; -import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.convert.encryption.EncryptingConverter; +import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; /** * {@link ExplicitlyEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java index 382d086d8e..926dfda369 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java @@ -35,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; import org.springframework.data.mongodb.test.util.MongoTestMappingContext; import org.springframework.expression.spel.support.StandardEvaluationContext; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java index 47283e061b..c39c768186 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java @@ -50,7 +50,9 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; +import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config; +import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.util.Lazy; import org.springframework.test.context.ContextConfiguration; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java index f84a0b3661..d830ce04b9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java @@ -40,6 +40,8 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; +import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.test.util.MongoTestMappingContext; @@ -125,7 +127,7 @@ void delegatesConversionOfEntityTypes() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getNestedFullyEncrypted); when(conversionContext.getProperty()).thenReturn(property); - doReturn(convertedValue).when(conversionContext).write(any(), eq(property.getTypeInformation())); + doReturn(convertedValue).when(conversionContext).write(any(), eq(property.getTypeInformation().getType())); ArgumentCaptor path = ArgumentCaptor.forClass(String.class); when(conversionContext.getValue(path.capture())).thenReturn("(ツ)"); @@ -158,7 +160,7 @@ void listsOfComplexTypesAreConvertedEntirely() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getListOfComplex); when(conversionContext.getProperty()).thenReturn(property); - doReturn(convertedValue1, convertedValue2).when(conversionContext).write(any(), eq(property.getTypeInformation())); + doReturn(convertedValue1, convertedValue2).when(conversionContext).write(any(), eq(property.getTypeInformation().getType())); JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField(); source1.unencryptedValue = "nested-unencrypted-1"; @@ -178,7 +180,7 @@ void simpleMapsAreConvertedEntirely() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfString); when(conversionContext.getProperty()).thenReturn(property); doReturn(new Document("k1", "v1").append("k2", "v2")).when(conversionContext).write(any(), - eq(property.getTypeInformation())); + eq(property.getTypeInformation().getType())); converter.write(Map.of("k1", "v1", "k2", "v2"), conversionContext); @@ -195,7 +197,7 @@ void complexMapsAreConvertedEntirely() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfComplex); when(conversionContext.getProperty()).thenReturn(property); doReturn(new Document("k1", convertedValue1).append("k2", convertedValue2)).when(conversionContext).write(any(), - eq(property.getTypeInformation())); + eq(property.getTypeInformation().getType())); JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField(); source1.unencryptedValue = "nested-unencrypted-1"; From df061f8d8784f4e9c92e9e20baba172ff7c7b263 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 8 Mar 2023 09:14:56 +0100 Subject: [PATCH 22/24] Update documentation --- src/main/asciidoc/index.adoc | 1 + .../asciidoc/reference/mongo-encryption.adoc | 156 ++++++++++++++++++ .../asciidoc/reference/mongo-json-schema.adoc | 29 ---- 3 files changed, 157 insertions(+), 29 deletions(-) create mode 100644 src/main/asciidoc/reference/mongo-encryption.adoc diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 2eb7f54327..93b7896d88 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -32,6 +32,7 @@ include::{spring-data-commons-docs}/auditing.adoc[leveloffset=+1] include::reference/mongo-auditing.adoc[leveloffset=+1] include::reference/mapping.adoc[leveloffset=+1] include::reference/sharding.adoc[leveloffset=+1] +include::reference/mongo-encryption.adoc[leveloffset=+1] include::reference/kotlin.adoc[leveloffset=+1] include::reference/jmx.adoc[leveloffset=+1] diff --git a/src/main/asciidoc/reference/mongo-encryption.adoc b/src/main/asciidoc/reference/mongo-encryption.adoc new file mode 100644 index 0000000000..80d7fc1d77 --- /dev/null +++ b/src/main/asciidoc/reference/mongo-encryption.adoc @@ -0,0 +1,156 @@ +[[mongo.encryption]] += Client Side Field Level Encryption (CSFLE) + +Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB. +Please make sure to read the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions. + +[NOTE] +==== +Make sure to set the drivers `com.mongodb.AutoEncryptionSettings` to use client-side encryption. +MongoDB does not support encryption for all field types. +Specific data types require deterministic encryption to preserve equality comparison functionality. +==== + +[[mongo.encryption.automatic]] +== Automatic Encryption + +MongoDB supports https://www.mongodb.com/docs/manual/core/csfle/[Client-Side Field Level Encryption] out of the box using the MongoDB driver with its Automatic Encryption feature. +Automatic Encryption requires a <> that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step. + +Please refer to the <> section for more information on defining a JSON Schema that holds encryption information. + +To make use of a the `MongoJsonSchema` it needs to be combined with `AutoEncryptionSettings` which can be done eg. via a `MongoClientSettingsBuilderCustomizer`. + +[source,java] +---- +@Bean +MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) { + return (builder) -> { + + // ... keyVaultCollection, kmsProvider, ... + + MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext); + MongoJsonSchema patientSchema = schemaCreator + .filter(MongoJsonSchemaCreator.encryptedOnly()) + .createSchemaFor(Patient.class); + + AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() + .keyVaultNamespace(keyVaultCollection) + .kmsProviders(kmsProviders) + .extraOptions(extraOpts) + .schemaMap(Collections.singletonMap("db.patient", patientSchema.schemaDocument().toBsonDocument())) + .build(); + + builder.autoEncryptionSettings(autoEncryptionSettings); + }; +} +---- + +[[mongo.encryption.explicit]] +== Explicit Encryption + +Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform en-/decryption tasks. +The `@ExplicitlyEncrypted` annotation is a combination of the `@Encrypted` annotation used for <> and a <>. +In other words, `@ExplicitlyEncrypted` uses existing building blocks and combines them to provide simplified support for explicit encryption. + +[NOTE] +==== +Fields annotated with `@ExplicitlyEncrypted` are always encrypted entirely as outlined in below. + +[source,java] +---- +@ExplicitlyEncrypted(...) +String simpleValue; <1> + +@ExplicitlyEncrypted(...) +Address address; <2> + +@ExplicitlyEncrypted(...) +List<...> list; <3> + +@ExplicitlyEncrypted(...) +Map<..., ...> mapOfString; <3> +---- +<1> Encrypts the value of the simple type eg. a `String` if not `null`. +<2> Encrypts the entire `Address` object and all its nested fields. To only encrypt parts of the `Address`, like `Address#street` the `street` field needs to be annotated. +<3> `Collection` like fields are encrypted entirely and not a value by value basis. +<4> `Map` like fields are encrypted entirely and not on a key/value basis. +==== + +Depending on the encryption algorithm MongoDB supports certain operations on an encrypted field using its https://www.mongodb.com/docs/manual/core/queryable-encryption/[Queryable Encryption] feature. +To pick a certain algorithm use `@ExplicitlyEncrypted(algorithm = ... )` and choose the required one via `EncryptionAlgorithms`. +Please read the https://www.mongodb.com/docs/manual/core/csfle/fundamentals/encryption-algorithms[Encryption Types] manual for more information on algorithms and their usage. + +To perform the actual encryption we do also need a Data Encryption Key (DEK). +Please refer to the https://www.mongodb.com/docs/manual/core/csfle/quick-start/#create-a-data-encryption-key[MongoDB Documentation] for more information on how to set up key management and create a Data Encryption Key. +The DEK can be referenced directly via its `id` or a defined _alternative name_. +The `@EncryptedField` annotation only allows referencing a DEK via an alternative name. +It is possible to provide an `EncryptionKeyResolver`, which will be discussed later, to any DEK. + +.Reference the Data Encryption Key +==== +[source,java] +---- +@EncryptedField(algorithm = ..., altKeyName = "secret-key") <1> +String ssn; +---- + +[source,java] +---- +@EncryptedField(algorithm = ..., altKeyName = "/name") <2> +String ssn; +---- +<1> Use the DEK stored with the alternative name `secret-key`. +<2> Uses a field reference that will read the actual field value and use that for key lookup. Always requires the full document to be present for save operations. Fields cannot be used in queries/aggregations. +==== + +By default the `@ExplicitlyEncrypted(value=...)` attribute will reference a `MongoEncryptionConverter`. +It is possible to change the default implementation and exchange it with any `PropertyValueConverter` implementation by providing the according type reference. +To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the <> section. + +[[mongo.encryption.explicit-setup]] +=== MongoEncryptionConverter Setup + +The default `MongoEncryptionConverter` needs to be registered within the `ApplicationContext`. +To do so we need to 1st setup the `Bean` and 2nd use a `BeanFactoryAwarePropertyValueConverterFactory` in the converter configuration. +The converter itself needs to know about the actual `Encryption` that is capable of en-/decrypting `BsonValue` to/from `BsonBinary` as well as a `EncryptionKeyResolver`. +`MongoClientEncryption` is the default implementation delegating en-/decryption to `com.mongodb.client.vault.ClientEncryption`. +The `EncryptionKeyResolver` provides the DEK to be used for encrypting the field. +Since the `@ExplicitlyEncrypted` annotation does not need to specify an alt key name the `EncryptionKeyResolver` receives the current `EncryptionContext` that provides access to the field for dynamic DEK resolution. +`EncryptionKeyResolver.annotationBased(...)` offers an implementation that will lookup values from the `@ExplicitlyEncrypted` annotation before falling back to the context based resolution. + +.Sample MongoEncryptionConverter Configuration +==== +[source,java] +---- +class Config extends AbstractMongoClientConfiguration { + + // ... + + @Autowired ApplicationContext appContext; + + @Bean + MongoEncryptionConverter encryptingConverter() { + + ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder() + // ... + + Encryption encryption = MongoClientEncryption.just(ClientEncryptions.create(encryptionSettings)) <1> + EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotationBased((ctx) -> ...); <2> + + return new MongoEncryptionConverter(encryption, keyResolver); <3> + } + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter adapter) { + + adapter + .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(appContext)); <4> + } +} +---- +<1> Set up a `com.mongodb.client.vault.ClientEncryption` specific `Encryption` engine. +<2> Read the `EncryptionKey` from annotations on the field. +<3> Create the `MongoEncryptionConverter`. +<4> Enable for a `PropertyValueConverter` within the `BeanFactory`. +==== diff --git a/src/main/asciidoc/reference/mongo-json-schema.adoc b/src/main/asciidoc/reference/mongo-json-schema.adoc index 26e8f7f099..369a7171dd 100644 --- a/src/main/asciidoc/reference/mongo-json-schema.adoc +++ b/src/main/asciidoc/reference/mongo-json-schema.adoc @@ -396,37 +396,8 @@ public class EncryptionExtension implements EvaluationContextExtension { } } ---- - -To combine derived encryption settings with `AutoEncryptionSettings` in a Spring Boot application use the `MongoClientSettingsBuilderCustomizer`. - -[source,java] ----- -@Bean -MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) { - return (builder) -> { - - // ... keyVaultCollection, kmsProvider, ... - - MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext); - MongoJsonSchema patientSchema = schemaCreator - .filter(MongoJsonSchemaCreator.encryptedOnly()) - .createSchemaFor(Patient.class); - - AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder() - .keyVaultNamespace(keyVaultCollection) - .kmsProviders(kmsProviders) - .extraOptions(extraOpts) - .schemaMap(Collections.singletonMap("db.patient", patientSchema.schemaDocument().toBsonDocument())) - .build(); - - builder.autoEncryptionSettings(autoEncryptionSettings); - }; -} ----- ==== -NOTE: Make sure to set the drivers `com.mongodb.AutoEncryptionSettings` to use client-side encryption. MongoDB does not support encryption for all field types. Specific data types require deterministic encryption to preserve equality comparison functionality. - [[mongo.jsonSchema.types]] ==== JSON Schema Types From 7c536befc90b39ed9c8a5cb8b63c6b5553104054 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 10 Mar 2023 08:11:18 +0100 Subject: [PATCH 23/24] Use property TypeInformation instead of Class for conversion to avoid generic type info loss. --- .../encryption/ExplicitEncryptionContext.java | 5 +- .../encryption/MongoEncryptionConverter.java | 8 +-- .../core/encryption/EncryptionContext.java | 58 ++++++++++++++++++- .../MongoEncryptionConverterUnitTests.java | 8 +-- 4 files changed, 66 insertions(+), 13 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java index abe89977b2..729a5ccd46 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java @@ -18,6 +18,7 @@ import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.encryption.EncryptionContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -58,12 +59,12 @@ public EvaluationContext getEvaluationContext(Object source) { } @Override - public T read(@Nullable Object value, @NonNull Class target) { + public T read(@Nullable Object value, TypeInformation target) { return conversionContext.read(value, target); } @Override - public T write(@Nullable Object value, @NonNull Class target) { + public T write(@Nullable Object value, TypeInformation target) { return conversionContext.write(value, target); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index e1418e6185..4db54a9f09 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -144,7 +144,7 @@ public Object encrypt(Object value, EncryptionContext context) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } if (persistentProperty.isMap()) { - Object convertedMap = context.write(value, persistentProperty.getTypeInformation().getType()); + Object convertedMap = context.write(value); if (convertedMap instanceof Document document) { return encryption.encrypt(document.toBsonDocument(), encryptionOptions); } @@ -155,7 +155,7 @@ public Object encrypt(Object value, EncryptionContext context) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } - Object write = context.write(value, persistentProperty.getTypeInformation().getType()); + Object write = context.write(value); if (write instanceof Document doc) { return encryption.encrypt(doc.toBsonDocument(), encryptionOptions); } @@ -178,12 +178,12 @@ public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty } else { if (value instanceof Collection values) { values.forEach(it -> { - Document write = (Document) context.write(it, property.getTypeInformation().getType()); + Document write = (Document) context.write(it, property.getTypeInformation()); bsonArray.add(write.toBsonDocument()); }); } else if (ObjectUtils.isArray(value)) { for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) context.write(o, property.getTypeInformation().getType()); + Document write = (Document) context.write(o, property.getTypeInformation()); bsonArray.add(write.toBsonDocument()); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index a11a5166bd..053b63a1f3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -16,9 +16,10 @@ package org.springframework.data.mongodb.core.encryption; import org.springframework.data.convert.ValueConversionContext; +import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; /** @@ -42,6 +43,17 @@ public interface EncryptionContext { */ Object convertToMongoType(Object value); + /** + * Reads the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}. + * + * @param value {@link Object value} to be read; can be {@literal null}. + * @return can be {@literal null}. + * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. + */ + default T read(@Nullable Object value) { + return (T) read(value, getProperty().getTypeInformation()); + } + /** * Reads the value as an instance of {@link Class type}. * @@ -50,7 +62,34 @@ public interface EncryptionContext { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - T read(@Nullable Object value, @NonNull Class target); + default T read(@Nullable Object value, Class target) { + return read(value, TypeInformation.of(target)); + } + + /** + * Reads the value as an instance of {@link TypeInformation type}. + * + * @param value {@link Object value} to be read; can be {@literal null}. + * @param target {@link TypeInformation type} of value to be read; must not be {@literal null}. + * @return can be {@literal null}. + * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. + */ + T read(@Nullable Object value, TypeInformation target); + + /** + * Write the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}. + * + * @param value {@link Object value} to write; can be {@literal null}. + * @return can be {@literal null}. + * @throws IllegalStateException if value cannot be written as an instance of the + * {@link PersistentProperty#getTypeInformation() property type}. + * @see PersistentProperty#getTypeInformation() + * @see #write(Object, TypeInformation) + */ + @Nullable + default T write(@Nullable Object value) { + return (T) write(value, getProperty().getTypeInformation()); + } /** * Write the value as an instance of {@link Class type}. @@ -61,7 +100,20 @@ public interface EncryptionContext { * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}. */ @Nullable - T write(@Nullable Object value, @NonNull Class target); + default T write(@Nullable Object value, Class target) { + return write(value, TypeInformation.of(target)); + } + + /** + * Write the value as an instance of given {@link TypeInformation type}. + * + * @param value {@link Object value} to write; can be {@literal null}. + * @param target {@link TypeInformation type} of value to be written; must not be {@literal null}. + * @return can be {@literal null}. + * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}. + */ + @Nullable + T write(@Nullable Object value, TypeInformation target); /** * Lookup the value for a given path within the current context. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java index d830ce04b9..7d405065ed 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java @@ -127,7 +127,7 @@ void delegatesConversionOfEntityTypes() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getNestedFullyEncrypted); when(conversionContext.getProperty()).thenReturn(property); - doReturn(convertedValue).when(conversionContext).write(any(), eq(property.getTypeInformation().getType())); + doReturn(convertedValue).when(conversionContext).write(any(), eq(property.getTypeInformation())); ArgumentCaptor path = ArgumentCaptor.forClass(String.class); when(conversionContext.getValue(path.capture())).thenReturn("(ツ)"); @@ -160,7 +160,7 @@ void listsOfComplexTypesAreConvertedEntirely() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getListOfComplex); when(conversionContext.getProperty()).thenReturn(property); - doReturn(convertedValue1, convertedValue2).when(conversionContext).write(any(), eq(property.getTypeInformation().getType())); + doReturn(convertedValue1, convertedValue2).when(conversionContext).write(any(), eq(property.getTypeInformation())); JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField(); source1.unencryptedValue = "nested-unencrypted-1"; @@ -180,7 +180,7 @@ void simpleMapsAreConvertedEntirely() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfString); when(conversionContext.getProperty()).thenReturn(property); doReturn(new Document("k1", "v1").append("k2", "v2")).when(conversionContext).write(any(), - eq(property.getTypeInformation().getType())); + eq(property.getTypeInformation())); converter.write(Map.of("k1", "v1", "k2", "v2"), conversionContext); @@ -197,7 +197,7 @@ void complexMapsAreConvertedEntirely() { MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfComplex); when(conversionContext.getProperty()).thenReturn(property); doReturn(new Document("k1", convertedValue1).append("k2", convertedValue2)).when(conversionContext).write(any(), - eq(property.getTypeInformation().getType())); + eq(property.getTypeInformation())); JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField(); source1.unencryptedValue = "nested-unencrypted-1"; From 3d2712a4a666659288eea175a01bafeb55906988 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 14 Mar 2023 15:10:44 +0100 Subject: [PATCH 24/24] Polishing. Remove caching variant of MongoClientEncryption. Rename types for consistent key alt name scheme. Rename annotation to ExplicitEncrypted. Add package-info. Improve documentation wording. Reduce visibility of KeyId and KeyAltName to package-private. --- .../core/convert/MappingMongoConverter.java | 27 +---- .../core/convert/MongoConversionContext.java | 15 ++- .../encryption/EncryptingConverter.java | 14 +-- .../encryption/MongoEncryptionConverter.java | 86 +++++++------ .../core/convert/encryption/package-info.java | 7 ++ .../mongodb/core/encryption/Encryption.java | 2 +- .../core/encryption/EncryptionContext.java | 6 +- .../core/encryption/EncryptionKey.java | 114 +++--------------- .../encryption/EncryptionKeyResolver.java | 42 ++++--- .../core/encryption/EncryptionOptions.java | 23 ++-- .../mongodb/core/encryption/KeyAltName.java | 54 +++++++++ .../data/mongodb/core/encryption/KeyId.java | 59 +++++++++ .../encryption/MongoClientEncryption.java | 41 +------ .../mongodb/core/encryption/package-info.java | 6 + ...yEncrypted.java => ExplicitEncrypted.java} | 19 +-- .../EncryptionKeyResolverUnitTests.java | 32 ++--- .../encryption/EncryptionKeyUnitTests.java | 10 +- .../core/encryption/EncryptionTests.java | 82 +++++++++---- .../MongoClientEncryptionUnitTests.java | 18 +-- .../MongoEncryptionConverterUnitTests.java | 32 ++--- .../asciidoc/reference/mongo-encryption.adoc | 106 +++++++++------- 21 files changed, 427 insertions(+), 368 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyAltName.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyId.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/{ExplicitlyEncrypted.java => ExplicitEncrypted.java} (84%) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 83f6db6406..fe1882dfdf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -17,16 +17,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; +import java.util.*; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -39,7 +30,6 @@ import org.bson.conversions.Bson; import org.bson.json.JsonReader; import org.bson.types.ObjectId; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; @@ -51,16 +41,7 @@ import org.springframework.data.annotation.Reference; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.TypeMapper; -import org.springframework.data.mapping.AccessOptions; -import org.springframework.data.mapping.Association; -import org.springframework.data.mapping.InstanceCreatorMetadata; -import org.springframework.data.mapping.MappingException; -import org.springframework.data.mapping.Parameter; -import org.springframework.data.mapping.PersistentEntity; -import org.springframework.data.mapping.PersistentProperty; -import org.springframework.data.mapping.PersistentPropertyAccessor; -import org.springframework.data.mapping.PersistentPropertyPath; -import org.springframework.data.mapping.PersistentPropertyPathAccessor; +import org.springframework.data.mapping.*; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; @@ -902,7 +883,7 @@ protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor acce if (conversions.hasValueConverter(prop)) { accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj, - new MongoConversionContext(new PropertyValueProvider() { + new MongoConversionContext(new PropertyValueProvider<>() { @Nullable @Override public T getPropertyValue(MongoPersistentProperty property) { @@ -1245,7 +1226,7 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist if (conversions.hasValueConverter(property)) { accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value, - new MongoConversionContext(new PropertyValueProvider() { + new MongoConversionContext(new PropertyValueProvider<>() { @Nullable @Override public T getPropertyValue(MongoPersistentProperty property) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index 30bd4c37df..c5b434bf4d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -15,17 +15,12 @@ */ package org.springframework.data.mongodb.core.convert; -import java.util.function.Supplier; - import org.bson.conversions.Bson; import org.springframework.data.convert.ValueConversionContext; -import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.expression.EvaluationContext; import org.springframework.lang.Nullable; /** @@ -36,18 +31,20 @@ */ public class MongoConversionContext implements ValueConversionContext { - private final PropertyValueProvider accessor; // TODO: generics + private final PropertyValueProvider accessor; // TODO: generics private final MongoPersistentProperty persistentProperty; private final MongoConverter mongoConverter; @Nullable private final SpELContext spELContext; - public MongoConversionContext(PropertyValueProvider accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { + public MongoConversionContext(PropertyValueProvider accessor, + MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { this(accessor, persistentProperty, mongoConverter, null); } - public MongoConversionContext(PropertyValueProvider accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, SpELContext spELContext) { + public MongoConversionContext(PropertyValueProvider accessor, + MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, @Nullable SpELContext spELContext) { this.accessor = accessor; this.persistentProperty = persistentProperty; @@ -60,11 +57,13 @@ public MongoPersistentProperty getProperty() { return persistentProperty; } + @Nullable public Object getValue(String propertyPath) { return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath)); } @Override + @SuppressWarnings("unchecked") public T write(@Nullable Object value, TypeInformation target) { return (T) mongoConverter.convertToMongoType(value, target); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java index 3bef1f280d..fba6907108 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java @@ -20,7 +20,7 @@ import org.springframework.data.mongodb.core.encryption.EncryptionContext; /** - * A specialized {@link MongoValueConverter} for {@literal en-/decrypting} properties. + * A specialized {@link MongoValueConverter} for {@literal encryptiong} and {@literal decrypting} properties. * * @author Christoph Strobl * @since 4.1 @@ -32,20 +32,20 @@ default S read(Object value, MongoConversionContext context) { return decrypt(value, buildEncryptionContext(context)); } - @Override - default T write(Object value, MongoConversionContext context) { - return encrypt(value, buildEncryptionContext(context)); - } - /** * Decrypt the given encrypted source value within the given {@link EncryptionContext context}. - * + * * @param encryptedValue the encrypted source. * @param context the context to operate in. * @return never {@literal null}. */ S decrypt(Object encryptedValue, EncryptionContext context); + @Override + default T write(Object value, MongoConversionContext context) { + return encrypt(value, buildEncryptionContext(context)); + } + /** * Encrypt the given raw source value within the given {@link EncryptionContext context}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index 4db54a9f09..bf7d87bef1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -39,6 +39,9 @@ import org.springframework.util.ObjectUtils; /** + * Default implementation of {@link EncryptingConverter}. Properties used with this converter must be annotated with + * {@link Encrypted @Encrypted} to provide key and algorithm metadata. + * * @author Christoph Strobl * @since 4.1 */ @@ -46,7 +49,7 @@ public class MongoEncryptionConverter implements EncryptingConverter encryption; + private final Encryption encryption; private final EncryptionKeyResolver keyResolver; public MongoEncryptionConverter(Encryption encryption, EncryptionKeyResolver keyResolver) { @@ -70,7 +73,7 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) { if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("Decrypting %s.%s.", getProperty(context).getOwner().getName(), + LOGGER.debug(String.format("Decrypting %s.%s.", getProperty(context).getOwner().getName(), getProperty(context).getName())); } @@ -83,18 +86,20 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } } - MongoPersistentProperty persistentProperty = getProperty(context); + MongoPersistentProperty persistentProperty = getProperty(context); if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable iterable) { + + int size = iterable instanceof Collection c ? c.size() : 10; + if (!persistentProperty.isEntity()) { - Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), size); iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); return collection; } else { - Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); + Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), size); iterable.forEach(it -> { - collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), - persistentProperty.getActualType())); + collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); }); return collection; } @@ -109,17 +114,12 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) { - return context.read(BsonUtils.toJavaType(bsonDocument), - persistentProperty.getTypeInformation().getType()); + return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation().getType()); } return decryptedValue; } - private MongoPersistentProperty getProperty(EncryptionContext context) { - return context.getProperty(); - } - @Override public Object encrypt(Object value, EncryptionContext context) { @@ -128,15 +128,19 @@ public Object encrypt(Object value, EncryptionContext context) { getProperty(context).getName())); } - MongoPersistentProperty persistentProperty = getProperty(context); + MongoPersistentProperty persistentProperty = getProperty(context); Encrypted annotation = persistentProperty.findAnnotation(Encrypted.class); - if(annotation == null) { + if (annotation == null) { annotation = persistentProperty.getOwner().findAnnotation(Encrypted.class); } - EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm()); - encryptionOptions.setKey(keyResolver.getKey(context)); + if (annotation == null) { + throw new IllegalStateException(String.format("Property %s.%s is not annotated with @Encrypted", + getProperty(context).getOwner().getName(), getProperty(context).getName())); + } + + EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm(), keyResolver.getKey(context)); if (!persistentProperty.isEntity()) { @@ -162,36 +166,44 @@ public Object encrypt(Object value, EncryptionContext context) { return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions); } - public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, + private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, EncryptionContext context) { BsonArray bsonArray = new BsonArray(); - if (!property.isEntity()) { - if (value instanceof Collection values) { - values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - bsonArray.add(BsonUtils.simpleToBsonValue(o)); + boolean isEntity = property.isEntity(); + + if (value instanceof Collection values) { + values.forEach(it -> { + + if (isEntity) { + Document document = (Document) context.write(it, property.getTypeInformation()); + bsonArray.add(document == null ? null : document.toBsonDocument()); + } else { + bsonArray.add(BsonUtils.simpleToBsonValue(it)); } - } - return bsonArray; - } else { - if (value instanceof Collection values) { - values.forEach(it -> { - Document write = (Document) context.write(it, property.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); - }); - } else if (ObjectUtils.isArray(value)) { - for (Object o : ObjectUtils.toObjectArray(value)) { - Document write = (Document) context.write(o, property.getTypeInformation()); - bsonArray.add(write.toBsonDocument()); + }); + } else if (ObjectUtils.isArray(value)) { + + for (Object o : ObjectUtils.toObjectArray(value)) { + + if (isEntity) { + Document document = (Document) context.write(o, property.getTypeInformation()); + bsonArray.add(document == null ? null : document.toBsonDocument()); + } else { + bsonArray.add(BsonUtils.simpleToBsonValue(o)); } } - return bsonArray; } + + return bsonArray; } + @Override public EncryptionContext buildEncryptionContext(MongoConversionContext context) { return new ExplicitEncryptionContext(context); } + + protected MongoPersistentProperty getProperty(EncryptionContext context) { + return context.getProperty(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java new file mode 100644 index 0000000000..4a6f78357a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java @@ -0,0 +1,7 @@ +/** + * Converters integrating with + * explicit encryption + * mechanism of Client-Side Field Level Encryption. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.mongodb.core.convert.encryption; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java index 70897d3cb7..6fdbcf24cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.encryption; /** - * Component responsible for en-/decrypting values. + * Component responsible for encrypting and decrypting values. * * @author Christoph Strobl * @since 4.1 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 053b63a1f3..6028341e13 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.core.encryption; -import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; @@ -23,7 +22,10 @@ import org.springframework.lang.Nullable; /** + * Context to encapsulate encryption for a specific {@link MongoPersistentProperty}. + * * @author Christoph Strobl + * @since 4.1 */ public interface EncryptionContext { @@ -36,7 +38,7 @@ public interface EncryptionContext { /** * Shortcut for converting a given {@literal value} into its store representation using the root - * {@link ValueConversionContext}. + * {@code ValueConversionContext}. * * @param value * @return diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java index febaa762b6..f4eebd43ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java @@ -16,128 +16,52 @@ package org.springframework.data.mongodb.core.encryption; import org.bson.BsonBinary; -import org.bson.BsonBinarySubType; -import org.springframework.util.ObjectUtils; +import org.springframework.util.Assert; /** * The {@link EncryptionKey} represents a {@literal Data Encryption Key} reference that can be either direct via the - * {@link KeyId key id} or its {@link AltKeyName Key Alternative Name}. - * + * {@link KeyId key id} or its {@link KeyAltName Key Alternative Name}. + * * @author Christoph Strobl * @since 4.1 */ public interface EncryptionKey { - /** - * @return the value that allows to reference a specific key - */ - Object value(); - - /** - * @return the {@link Type} of reference. - */ - Type type(); - /** * Create a new {@link EncryptionKey} that uses the keys id for reference. * * @param key must not be {@literal null}. - * @return new instance of {@link KeyId}. + * @return new instance of {@link EncryptionKey KeyId}. */ - static KeyId keyId(BsonBinary key) { + static EncryptionKey keyId(BsonBinary key) { + + Assert.notNull(key, "KeyId must not be null"); + return new KeyId(key); } /** * Create a new {@link EncryptionKey} that uses an {@literal Key Alternative Name} for reference. * - * @param altKeyName must not be {@literal null}. - * @return new instance of {@link KeyId}. - */ - static AltKeyName altKeyName(String altKeyName) { - return new AltKeyName(altKeyName); - } - - /** - * @param value must not be {@literal null}. + * @param keyAltName must not be {@literal null} or empty. + * @return new instance of {@link EncryptionKey KeyAltName}. */ - record KeyId(BsonBinary value) implements EncryptionKey { - - @Override - public Type type() { - return Type.ID; - } + static EncryptionKey keyAltName(String keyAltName) { - @Override - public String toString() { + Assert.hasText(keyAltName, "Key Alternative Name must not be empty"); - if (BsonBinarySubType.isUuid(value.getType())) { - String representation = value.asUuid().toString(); - if (representation.length() > 6) { - return String.format("KeyId('%s***')", representation.substring(0, 6)); - } - } - return "KeyId('***')"; - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - KeyId that = (KeyId) o; - return ObjectUtils.nullSafeEquals(value, that.value); - } - - @Override - public int hashCode() { - return ObjectUtils.nullSafeHashCode(value); - } + return new KeyAltName(keyAltName); } /** - * @param value must not be {@literal null}. + * @return the value that allows to reference a specific key. */ - record AltKeyName(String value) implements EncryptionKey { - - @Override - public Type type() { - return Type.ALT; - } - - @Override - public String toString() { - - if (value().length() <= 3) { - return "AltKeyName('***')"; - } - return String.format("AltKeyName('%s***')", value.substring(0, 3)); - } - - @Override - public boolean equals(Object o) { - - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - - AltKeyName that = (AltKeyName) o; - return ObjectUtils.nullSafeEquals(value, that.value); - } + Object value(); - @Override - public int hashCode() { - return ObjectUtils.nullSafeHashCode(value); - } - } + /** + * @return the {@link Type} of reference. + */ + Type type(); /** * The key reference type. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java index 61deacff5e..c9bc2b07ec 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java @@ -18,18 +18,20 @@ import org.bson.BsonBinary; import org.bson.types.Binary; import org.springframework.data.mongodb.core.mapping.Encrypted; -import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; +import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.encryption.EncryptionUtils; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * Interface to obtain a {@link EncryptionKey Data Encryption Key} that is valid in a given {@link EncryptionContext * context}. *

- * Use the {@link #annotationBased(EncryptionKeyResolver) based} variant which will first try to resolve a potential - * {@link ExplicitlyEncrypted#altKeyName() Key Alternate Name} from annotations before calling the fallback resolver. - * + * Use the {@link #annotated(EncryptionKeyResolver) based} variant which will first try to resolve a potential + * {@link ExplicitEncrypted#keyAltName() Key Alternate Name} from annotations before calling the fallback resolver. + * * @author Christoph Strobl * @since 4.1 * @see EncryptionKey @@ -46,20 +48,23 @@ public interface EncryptionKeyResolver { EncryptionKey getKey(EncryptionContext encryptionContext); /** - * Obtain an {@link EncryptionKeyResolver} that evaluates {@link ExplicitlyEncrypted#altKeyName()} and only calls the + * Obtain an {@link EncryptionKeyResolver} that evaluates {@link ExplicitEncrypted#keyAltName()} and only calls the * fallback {@link EncryptionKeyResolver resolver} if no {@literal Key Alternate Name} is present. - * + * * @param fallback must not be {@literal null}. * @return new instance of {@link EncryptionKeyResolver}. */ - static EncryptionKeyResolver annotationBased(EncryptionKeyResolver fallback) { + static EncryptionKeyResolver annotated(EncryptionKeyResolver fallback) { + + Assert.notNull(fallback, "Fallback EncryptionKeyResolver must not be nul"); return ((encryptionContext) -> { - ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); - if (annotation == null || !StringUtils.hasText(annotation.altKeyName())) { + MongoPersistentProperty property = encryptionContext.getProperty(); + ExplicitEncrypted annotation = property.findAnnotation(ExplicitEncrypted.class); + if (annotation == null || !StringUtils.hasText(annotation.keyAltName())) { - Encrypted encrypted = encryptionContext.getProperty().getOwner().findAnnotation(Encrypted.class); + Encrypted encrypted = property.getOwner().findAnnotation(Encrypted.class); if (encrypted == null) { return fallback.getKey(encryptionContext); } @@ -73,19 +78,22 @@ static EncryptionKeyResolver annotationBased(EncryptionKeyResolver fallback) { return EncryptionKey.keyId((BsonBinary) BsonUtils.simpleToBsonValue(binary)); } if (o instanceof String string) { - return EncryptionKey.altKeyName(string); + return EncryptionKey.keyAltName(string); } + + throw new IllegalStateException(String.format("Cannot determine encryption key for %s.%s using key type %s", + property.getOwner().getName(), property.getName(), o == null ? "null" : o.getClass().getName())); } - String altKeyName = annotation.altKeyName(); - if (altKeyName.startsWith("/")) { - Object fieldValue = encryptionContext.lookupValue(altKeyName.replace("/", "")); + String keyAltName = annotation.keyAltName(); + if (keyAltName.startsWith("/")) { + Object fieldValue = encryptionContext.lookupValue(keyAltName.replace("/", "")); if (fieldValue == null) { - throw new IllegalStateException(String.format("Key Alternative Name for %s was null", altKeyName)); + throw new IllegalStateException(String.format("Key Alternative Name for %s was null", keyAltName)); } - return new EncryptionKey.AltKeyName(fieldValue.toString()); + return new KeyAltName(fieldValue.toString()); } else { - return new EncryptionKey.AltKeyName(altKeyName); + return new KeyAltName(keyAltName); } }); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index 37805ecec2..e0480b6f1d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.encryption; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** @@ -27,16 +27,15 @@ public class EncryptionOptions { private final String algorithm; - private @Nullable EncryptionKey key; + private final EncryptionKey key; - public EncryptionOptions(String algorithm) { - this.algorithm = algorithm; - } + public EncryptionOptions(String algorithm, EncryptionKey key) { - public EncryptionOptions setKey(EncryptionKey key) { + Assert.hasText(algorithm, "Algorithm must not be empty"); + Assert.notNull(key, "EncryptionKey must not be empty"); this.key = key; - return this; + this.algorithm = algorithm; } public EncryptionKey key() { @@ -47,11 +46,6 @@ public String algorithm() { return algorithm; } - @Override - public String toString() { - return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}'; - } - @Override public boolean equals(Object o) { @@ -77,4 +71,9 @@ public int hashCode() { result = 31 * result + ObjectUtils.nullSafeHashCode(key); return result; } + + @Override + public String toString() { + return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}'; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyAltName.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyAltName.java new file mode 100644 index 0000000000..d6f8430148 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyAltName.java @@ -0,0 +1,54 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import org.springframework.util.ObjectUtils; + +record KeyAltName(String value) implements EncryptionKey { + + @Override + public Type type() { + return Type.ALT; + } + + @Override + public String toString() { + + if (value().length() <= 3) { + return "KeyAltName('***')"; + } + return String.format("KeyAltName('%s***')", value.substring(0, 3)); + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + KeyAltName that = (KeyAltName) o; + return ObjectUtils.nullSafeEquals(value, that.value); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(value); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyId.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyId.java new file mode 100644 index 0000000000..7136b72497 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyId.java @@ -0,0 +1,59 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.mongodb.core.encryption; + +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.springframework.util.ObjectUtils; + +record KeyId(BsonBinary value) implements EncryptionKey { + + @Override + public Type type() { + return Type.ID; + } + + @Override + public String toString() { + + if (BsonBinarySubType.isUuid(value.getType())) { + String representation = value.asUuid().toString(); + if (representation.length() > 6) { + return String.format("KeyId('%s***')", representation.substring(0, 6)); + } + } + return "KeyId('***')"; + } + + @Override + public boolean equals(Object o) { + + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + org.springframework.data.mongodb.core.encryption.KeyId that = (org.springframework.data.mongodb.core.encryption.KeyId) o; + return ObjectUtils.nullSafeEquals(value, that.value); + } + + @Override + public int hashCode() { + return ObjectUtils.nullSafeHashCode(value); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java index 6f1f5a2bd0..dc34515fe1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.core.encryption; -import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; import org.bson.BsonBinary; @@ -35,23 +34,9 @@ public class MongoClientEncryption implements Encryption { private final Supplier source; - private final AtomicReference cached; - - private MongoClientEncryption(Supplier source) { + MongoClientEncryption(Supplier source) { this.source = source; - this.cached = new AtomicReference<>(source.get()); - } - - /** - * The caching {@link MongoClientEncryption} variant caches and reuses the {@link ClientEncryption} obtained from the - * {@link Supplier} until explicitly {@link #refresh() refreshed}. - * - * @param clientEncryption must not be {@literal null} nor emit {@literal null}. - * @return new instance of {@link MongoClientEncryption}. - */ - public static MongoClientEncryption caching(Supplier clientEncryption) { - return new MongoClientEncryption(clientEncryption); } /** @@ -64,27 +49,7 @@ public static MongoClientEncryption just(ClientEncryption clientEncryption) { Assert.notNull(clientEncryption, "ClientEncryption must not be null"); - return new MongoClientEncryption(() -> clientEncryption) { - @Override - public boolean refresh() { - return false; - } - }; - } - - /** - * @return {@literal true} if refreshed, {@literal false} otherwise. - */ - public boolean refresh() { - cached.set(source.get()); - return true; - } - - /** - * {@link ClientEncryption#close() Shutdown} the underlying {@link ClientEncryption}. - */ - public void shutdown() { - getClientEncryption().close(); + return new MongoClientEncryption(() -> clientEncryption); } @Override @@ -107,7 +72,7 @@ public BsonBinary encrypt(BsonValue value, EncryptionOptions options) { } public ClientEncryption getClientEncryption() { - return cached.get(); + return source.get(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java new file mode 100644 index 0000000000..f3906d89dd --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java @@ -0,0 +1,6 @@ +/** + * Infrastructure for explicit + * encryption mechanism of Client-Side Field Level Encryption. + */ +@org.springframework.lang.NonNullApi +package org.springframework.data.mongodb.core.encryption; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitlyEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java similarity index 84% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitlyEncrypted.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java index f64d1ed144..3208b461b2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitlyEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java @@ -27,13 +27,13 @@ import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; /** - * {@link ExplicitlyEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that - * indicates the target element is subject to encryption during the mapping process, in which a given domain type is - * converted into the store specific format. + * {@link ExplicitEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that indicates + * the target element is subject to encryption during the mapping process, in which a given domain type is converted + * into the store specific format. *

* The {@link #value()} attribute, defines the bean type to look up within the * {@link org.springframework.context.ApplicationContext} to obtain the {@link EncryptingConverter} responsible for the - * actual {@literal en-/decryption} while {@link #algorithm()} and {@link #altKeyName()} can be used to define aspects + * actual {@literal en-/decryption} while {@link #algorithm()} and {@link #keyAltName()} can be used to define aspects * of the encryption process. * *

@@ -41,11 +41,11 @@
  * 	private ObjectId id;
  * 	private String name;
  *
- * 	@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "secred-key-alternative-name") //
+ * 	@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "secred-key-alternative-name") //
  * 	private String ssn;
  * }
  * 
- * + * * @author Christoph Strobl * @since 4.1 * @see ValueConverter @@ -54,7 +54,7 @@ @Target(ElementType.FIELD) @Encrypted @ValueConverter -public @interface ExplicitlyEncrypted { +public @interface ExplicitEncrypted { /** * Define the algorithm to use. @@ -66,6 +66,7 @@ * objects and arrays as well as the query limitations that come with each of them. * * @return the string representation of the encryption algorithm to use. + * @see org.springframework.data.mongodb.core.EncryptionAlgorithms */ @AliasFor(annotation = Encrypted.class, value = "algorithm") String algorithm() default ""; @@ -78,10 +79,10 @@ * It is possible to use the {@literal "/"} character as a prefix to access a particular field value in the same * domain type. In this case {@code "/name"} references the value of the {@literal name} field. Please note that * update operations will require the full object to resolve those values. - * + * * @return the {@literal Key Alternate Name} if set or an empty {@link String}. */ - String altKeyName() default ""; + String keyAltName() default ""; /** * The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java index 926dfda369..bfe69deaf7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java @@ -35,11 +35,13 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.data.mongodb.core.mapping.Encrypted; -import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; +import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; import org.springframework.data.mongodb.test.util.MongoTestMappingContext; import org.springframework.expression.spel.support.StandardEvaluationContext; /** + * Unit tests for {@link EncryptionKeyResolver}. + * * @author Christoph Strobl */ @ExtendWith(MockitoExtension.class) @@ -65,7 +67,7 @@ void usesDefaultKeyIfNoAnnotationPresent() { EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, AnnotatedWithExplicitlyEncrypted::getNotAnnotated); - EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx); assertThat(key).isSameAs(defaultEncryptionKey); } @@ -76,7 +78,7 @@ void usesDefaultKeyIfAnnotatedValueIsEmpty() { EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, AnnotatedWithExplicitlyEncrypted::getAlgorithm); - EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx); assertThat(key).isSameAs(defaultEncryptionKey); } @@ -87,9 +89,9 @@ void usesDefaultAltKeyNameIfPresent() { EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyName); - EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx); - assertThat(key).isEqualTo(EncryptionKey.altKeyName("sec-key-name")); + assertThat(key).isEqualTo(EncryptionKey.keyAltName("sec-key-name")); } @Test // GH-4284 @@ -99,9 +101,9 @@ void readsAltKeyNameFromContextIfReferencingPropertyValue() { AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyNameFromPropertyValue); when(ctx.lookupValue(eq("notAnnotated"))).thenReturn("born-to-be-wild"); - EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx); - assertThat(key).isEqualTo(EncryptionKey.altKeyName("born-to-be-wild")); + assertThat(key).isEqualTo(EncryptionKey.keyAltName("born-to-be-wild")); } @Test // GH-4284 @@ -111,7 +113,7 @@ void readsKeyIdFromEncryptedAnnotationIfNoBetterCandidateAvailable() { AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType.class, AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType::getKeyIdFromDomainType); - EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx); assertThat(key).isEqualTo(EncryptionKey.keyId( new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g==")))); @@ -127,7 +129,7 @@ void ignoresKeyIdFromEncryptedAnnotationWhenBetterCandidateAvailable() { when(ctx.getEvaluationContext(any())).thenReturn(evaluationContext); - EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); + EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx); assertThat(key).isEqualTo(EncryptionKey.keyId( new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g==")))); @@ -145,13 +147,13 @@ class AnnotatedWithExplicitlyEncrypted { String notAnnotated; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // String algorithm; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "sec-key-name") // String algorithmAndAltKeyName; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/notAnnotated") // String algorithmAndAltKeyNameFromPropertyValue; } @@ -159,10 +161,10 @@ class AnnotatedWithExplicitlyEncrypted { @Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==") class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType { - @ExplicitlyEncrypted // + @ExplicitEncrypted // String keyIdFromDomainType; - @ExplicitlyEncrypted(altKeyName = "sec-key-name") // + @ExplicitEncrypted(keyAltName = "sec-key-name") // String altKeyNameFromPropertyIgnoringKeyIdFromDomainType; } @@ -170,7 +172,7 @@ class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationO @Encrypted(keyId = "#{#myKeyId}") class KeyIdFromSpel { - @ExplicitlyEncrypted // + @ExplicitEncrypted // String keyIdFromDomainType; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java index 1098f4c2be..2083aa1274 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java @@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test; /** + * Unit tests for {@link EncryptionKey}. + * * @author Christoph Strobl */ class EncryptionKeyUnitTests { @@ -40,9 +42,9 @@ void keyIdToStringDoesNotRevealEntireKey() { @Test // GH-4284 void altKeyNameToStringDoesNotRevealEntireKey() { - assertThat(EncryptionKey.altKeyName("s").toString()).contains("***"); - assertThat(EncryptionKey.altKeyName("su").toString()).contains("***"); - assertThat(EncryptionKey.altKeyName("sup").toString()).contains("***"); - assertThat(EncryptionKey.altKeyName("super-secret-key").toString()).contains("sup***"); + assertThat(EncryptionKey.keyAltName("s").toString()).contains("***"); + assertThat(EncryptionKey.keyAltName("su").toString()).contains("***"); + assertThat(EncryptionKey.keyAltName("sup").toString()).contains("***"); + assertThat(EncryptionKey.keyAltName("super-secret-key").toString()).contains("sup***"); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java index c39c768186..9cbe56ceeb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java @@ -30,8 +30,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import org.assertj.core.api.Assertions; import org.bson.BsonBinary; @@ -39,6 +41,7 @@ import org.bson.types.Binary; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -52,7 +55,7 @@ import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config; -import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; +import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.util.Lazy; import org.springframework.test.context.ContextConfiguration; @@ -68,6 +71,7 @@ import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.Indexes; import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryptions; /** @@ -80,7 +84,7 @@ public class EncryptionTests { @Autowired MongoTemplate template; @Test // GH-4284 - void enDeCryptSimpleValue() { + void encryptAndDecryptSimpleValue() { Person source = new Person(); source.id = "id-1"; @@ -95,7 +99,7 @@ void enDeCryptSimpleValue() { } @Test // GH-4284 - void enDeCryptComplexValue() { + void encryptAndDecryptComplexValue() { Person source = new Person(); source.id = "id-1"; @@ -112,7 +116,7 @@ void enDeCryptComplexValue() { } @Test // GH-4284 - void enDeCryptValueWithinComplexOne() { + void encryptAndDecryptValueWithinComplexOne() { Person source = new Person(); source.id = "id-1"; @@ -135,7 +139,7 @@ void enDeCryptValueWithinComplexOne() { } @Test // GH-4284 - void enDeCryptListOfSimpleValue() { + void encryptAndDecryptListOfSimpleValue() { Person source = new Person(); source.id = "id-1"; @@ -150,7 +154,7 @@ void enDeCryptListOfSimpleValue() { } @Test // GH-4284 - void enDeCryptListOfComplexValue() { + void encryptAndDecryptListOfComplexValue() { Person source = new Person(); source.id = "id-1"; @@ -170,7 +174,7 @@ void enDeCryptListOfComplexValue() { } @Test // GH-4284 - void enDeCryptMapOfSimpleValues() { + void encryptAndDecryptMapOfSimpleValues() { Person source = new Person(); source.id = "id-1"; @@ -185,7 +189,7 @@ void enDeCryptMapOfSimpleValues() { } @Test // GH-4284 - void enDeCryptMapOfComplexValues() { + void encryptAndDecryptMapOfComplexValues() { Person source = new Person(); source.id = "id-1"; @@ -312,7 +316,7 @@ void aggregationWithMatch() { } @Test - void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) throws InterruptedException { + void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException { BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local", new DataKeyOptions().keyAltNames(Collections.singletonList("user-1"))); @@ -349,7 +353,7 @@ void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) thr mongoClientEncryption.getClientEncryption().deleteKey(user2key); // clear the 60 second key cache within the mongo client - mongoClientEncryption.refresh(); + mongoClientEncryption.destroy(); assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1); @@ -444,12 +448,12 @@ MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEn new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); return new MongoEncryptionConverter(mongoClientEncryption, - EncryptionKeyResolver.annotationBased((ctx) -> EncryptionKey.keyId(dataKey.get()))); + EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get()))); } @Bean - MongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { - return MongoClientEncryption.caching(() -> ClientEncryptions.create(encryptionSettings)); + CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { + return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings)); } @Bean @@ -468,9 +472,9 @@ ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) { final byte[] localMasterKey = new byte[96]; new SecureRandom().nextBytes(localMasterKey); - Map> kmsProviders = new HashMap>() { + Map> kmsProviders = new HashMap<>() { { - put("local", new HashMap() { + put("local", new HashMap<>() { { put("key", localMasterKey); } @@ -485,6 +489,36 @@ ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) { .keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build(); return clientEncryptionSettings; } + + } + + static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean { + + static final AtomicReference cache = new AtomicReference<>(); + + CachingMongoClientEncryption(Supplier source) { + super(() -> { + + if (cache.get() != null) { + return cache.get(); + } + + ClientEncryption clientEncryption = source.get(); + cache.set(clientEncryption); + + return clientEncryption; + }); + } + + @Override + public void destroy() { + + ClientEncryption clientEncryption = cache.get(); + if (clientEncryption != null) { + clientEncryption.close(); + cache.set(null); + } + } } @Data @@ -494,30 +528,30 @@ static class Person { String id; String name; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // String ssn; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "mySuperSecretKey") // String wallet; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random Address address; AddressWithEncryptedZip encryptedZip; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random List listOfString; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random List
listOfComplex; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/name") // String viaAltKeyNameField; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // Map mapOfString; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // Map mapOfComplex; } @@ -531,7 +565,7 @@ static class Address { @Setter static class AddressWithEncryptedZip extends Address { - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip; + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip; @Override public String toString() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java index 4b087ef42e..65ec88e400 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java @@ -33,6 +33,8 @@ import com.mongodb.client.vault.ClientEncryption; /** + * Unit tests for {@link MongoClientEncryption}. + * * @author Christoph Strobl */ @ExtendWith(MockitoExtension.class) @@ -55,7 +57,7 @@ void delegatesEncrypt() { MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption); mce.encrypt(new BsonBinary(new byte[0]), - new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name"))); + new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random, EncryptionKey.keyAltName("sec-key-name"))); ArgumentCaptor options = ArgumentCaptor.forClass(EncryptOptions.class); verify(clientEncryption).encrypt(any(), options.capture()); @@ -63,23 +65,12 @@ void delegatesEncrypt() { assertThat(options.getValue().getKeyAltName()).isEqualTo("sec-key-name"); } - @Test // GH-4284 - void refreshHasNoEffectForFixedClientEncryption() { - - MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption); - mce.decrypt(new BsonBinary(new byte[0])); - - assertThat(mce.getClientEncryption()).isSameAs(clientEncryption); - assertThat(mce.refresh()).isFalse(); - assertThat(mce.getClientEncryption()).isSameAs(clientEncryption); - } - @Test // GH-4284 void refreshObtainsNextInstanceFromSupplier() { ClientEncryption next = mock(ClientEncryption.class); - MongoClientEncryption mce = MongoClientEncryption.caching(new Supplier<>() { + MongoClientEncryption mce = new MongoClientEncryption(new Supplier<>() { int counter = 0; @@ -90,7 +81,6 @@ public ClientEncryption get() { }); assertThat(mce.getClientEncryption()).isSameAs(clientEncryption); - assertThat(mce.refresh()).isTrue(); assertThat(mce.getClientEncryption()).isSameAs(next); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java index 7d405065ed..f94c088285 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java @@ -41,7 +41,7 @@ import org.mockito.quality.Strictness; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; -import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted; +import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.test.util.MongoTestMappingContext; @@ -72,10 +72,10 @@ class MongoEncryptionConverterUnitTests { @BeforeEach void beforeEach() { - when(fallbackKeyResolver.getKey(any())).thenReturn(EncryptionKey.altKeyName("default")); + when(fallbackKeyResolver.getKey(any())).thenReturn(EncryptionKey.keyAltName("default")); when(encryption.encrypt(valueToBeEncrypted.capture(), encryptionOptions.capture())) .thenReturn(new BsonBinary(new byte[0])); - keyResolver = EncryptionKeyResolver.annotationBased(fallbackKeyResolver); + keyResolver = EncryptionKeyResolver.annotated(fallbackKeyResolver); converter = new MongoEncryptionConverter(encryption, keyResolver); } @@ -89,7 +89,7 @@ void delegatesConversionOfSimpleValueWithDefaultEncryptionKeyFromKeyResolver() { assertThat(valueToBeEncrypted.getValue()).isEqualTo(new BsonString("foo")); assertThat(encryptionOptions.getValue()).isEqualTo( - new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic).setKey(EncryptionKey.altKeyName("default"))); + new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, EncryptionKey.keyAltName("default"))); } @Test // GH-4284 @@ -101,7 +101,7 @@ void favorsAltKeyNameIfPresent() { converter.write("foo", conversionContext); assertThat(encryptionOptions.getValue()).isEqualTo( - new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name"))); + new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random, EncryptionKey.keyAltName("sec-key-name"))); } @Test // GH-4284 @@ -117,7 +117,7 @@ void readsAltKeyNameFromProperty() { assertThat(path.getValue()).isEqualTo("notAnnotated"); assertThat(encryptionOptions.getValue()) - .isEqualTo(new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("(ツ)"))); + .isEqualTo(new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random, EncryptionKey.keyAltName("(ツ)"))); } @Test // GH-4284 @@ -216,33 +216,33 @@ static class Type { String notAnnotated; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // String stringValueWithAlgorithmOnly; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "sec-key-name") // String stringValueWithAlgorithmAndAltKeyName; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/notAnnotated") // String stringValueWithAlgorithmAndAltKeyNameFromPropertyValue; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random JustATypeWithAnUnencryptedField nestedFullyEncrypted; NestedWithEncryptedField nestedWithEncryptedField; // Client-Side Field Level Encryption does not support encrypting individual array elements - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // List listOfString; // Client-Side Field Level Encryption does not support encrypting individual array elements - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random List listOfComplex; // just as it was a domain type encrypt the entire thing here - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // Map mapOfString; - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // Map mapOfComplex; RecordWithEncryptedValue recordWithEncryptedValue; @@ -257,10 +257,10 @@ static class JustATypeWithAnUnencryptedField { static class NestedWithEncryptedField extends JustATypeWithAnUnencryptedField { - @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // String encryptedValue; } - record RecordWithEncryptedValue(@ExplicitlyEncrypted String value) { + record RecordWithEncryptedValue(@ExplicitEncrypted String value) { } } diff --git a/src/main/asciidoc/reference/mongo-encryption.adoc b/src/main/asciidoc/reference/mongo-encryption.adoc index 80d7fc1d77..41c470f763 100644 --- a/src/main/asciidoc/reference/mongo-encryption.adoc +++ b/src/main/asciidoc/reference/mongo-encryption.adoc @@ -2,7 +2,7 @@ = Client Side Field Level Encryption (CSFLE) Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB. -Please make sure to read the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions. +We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data. [NOTE] ==== @@ -49,39 +49,42 @@ MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) { [[mongo.encryption.explicit]] == Explicit Encryption -Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform en-/decryption tasks. -The `@ExplicitlyEncrypted` annotation is a combination of the `@Encrypted` annotation used for <> and a <>. -In other words, `@ExplicitlyEncrypted` uses existing building blocks and combines them to provide simplified support for explicit encryption. +Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks. +The `@ExplicitEncrypted` annotation is a combination of the `@Encrypted` annotation used for <> and a <>. +In other words, `@ExplicitEncrypted` uses existing building blocks to combine them for simplified explicit encryption support. [NOTE] ==== -Fields annotated with `@ExplicitlyEncrypted` are always encrypted entirely as outlined in below. +Fields annotated with `@ExplicitEncrypted` are always encrypted as whole. +Consider the following example: [source,java] ---- -@ExplicitlyEncrypted(...) -String simpleValue; <1> +@ExplicitEncrypted(…) +String simpleValue; <1> -@ExplicitlyEncrypted(...) -Address address; <2> +@ExplicitEncrypted(…) +Address address; <2> -@ExplicitlyEncrypted(...) -List<...> list; <3> +@ExplicitEncrypted(…) +List<...> list; <3> -@ExplicitlyEncrypted(...) -Map<..., ...> mapOfString; <3> +@ExplicitEncrypted(…) +Map<..., ...> mapOfString; <4> ---- -<1> Encrypts the value of the simple type eg. a `String` if not `null`. -<2> Encrypts the entire `Address` object and all its nested fields. To only encrypt parts of the `Address`, like `Address#street` the `street` field needs to be annotated. -<3> `Collection` like fields are encrypted entirely and not a value by value basis. -<4> `Map` like fields are encrypted entirely and not on a key/value basis. + +<1> Encrypts the value of the simple type such as a `String` if not `null`. +<2> Encrypts the entire `Address` object and all its nested fields as `Document`. +To only encrypt parts of the `Address`, like `Address#street` the `street` field within `Address` needs to be annotated with `@ExplicitEncrypted`. +<3> ``Collection``-like fields are encrypted as single value and not per entry. +<4> ``Map``-like fields are encrypted as single value and not as a key/value entry. ==== -Depending on the encryption algorithm MongoDB supports certain operations on an encrypted field using its https://www.mongodb.com/docs/manual/core/queryable-encryption/[Queryable Encryption] feature. -To pick a certain algorithm use `@ExplicitlyEncrypted(algorithm = ... )` and choose the required one via `EncryptionAlgorithms`. +Depending on the encryption algorithm, MongoDB supports certain operations on an encrypted field using its https://www.mongodb.com/docs/manual/core/queryable-encryption/[Queryable Encryption] feature. +To pick a certain algorithm use `@ExplicitEncrypted(algorithm)`, see `EncryptionAlgorithms` for algorithm constants. Please read the https://www.mongodb.com/docs/manual/core/csfle/fundamentals/encryption-algorithms[Encryption Types] manual for more information on algorithms and their usage. -To perform the actual encryption we do also need a Data Encryption Key (DEK). +To perform the actual encryption we require a Data Encryption Key (DEK). Please refer to the https://www.mongodb.com/docs/manual/core/csfle/quick-start/#create-a-data-encryption-key[MongoDB Documentation] for more information on how to set up key management and create a Data Encryption Key. The DEK can be referenced directly via its `id` or a defined _alternative name_. The `@EncryptedField` annotation only allows referencing a DEK via an alternative name. @@ -91,33 +94,38 @@ It is possible to provide an `EncryptionKeyResolver`, which will be discussed la ==== [source,java] ---- -@EncryptedField(algorithm = ..., altKeyName = "secret-key") <1> +@EncryptedField(algorithm=…, altKeyName = "secret-key") <1> String ssn; ---- [source,java] ---- -@EncryptedField(algorithm = ..., altKeyName = "/name") <2> +@EncryptedField(algorithm=…, altKeyName = "/name") <2> String ssn; ---- + <1> Use the DEK stored with the alternative name `secret-key`. -<2> Uses a field reference that will read the actual field value and use that for key lookup. Always requires the full document to be present for save operations. Fields cannot be used in queries/aggregations. +<2> Uses a field reference that will read the actual field value and use that for key lookup. +Always requires the full document to be present for save operations. +Fields cannot be used in queries/aggregations. ==== -By default the `@ExplicitlyEncrypted(value=...)` attribute will reference a `MongoEncryptionConverter`. +By default, the `@ExplicitEncrypted(value=…)` attribute references a `MongoEncryptionConverter`. It is possible to change the default implementation and exchange it with any `PropertyValueConverter` implementation by providing the according type reference. To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the <> section. [[mongo.encryption.explicit-setup]] === MongoEncryptionConverter Setup -The default `MongoEncryptionConverter` needs to be registered within the `ApplicationContext`. -To do so we need to 1st setup the `Bean` and 2nd use a `BeanFactoryAwarePropertyValueConverterFactory` in the converter configuration. -The converter itself needs to know about the actual `Encryption` that is capable of en-/decrypting `BsonValue` to/from `BsonBinary` as well as a `EncryptionKeyResolver`. -`MongoClientEncryption` is the default implementation delegating en-/decryption to `com.mongodb.client.vault.ClientEncryption`. -The `EncryptionKeyResolver` provides the DEK to be used for encrypting the field. -Since the `@ExplicitlyEncrypted` annotation does not need to specify an alt key name the `EncryptionKeyResolver` receives the current `EncryptionContext` that provides access to the field for dynamic DEK resolution. -`EncryptionKeyResolver.annotationBased(...)` offers an implementation that will lookup values from the `@ExplicitlyEncrypted` annotation before falling back to the context based resolution. +The converter setup for `MongoEncryptionConverter` requires a few steps as several components are involved. +The bean setup consists of the following: + +1. The `ClientEncryption` engine +2. A `MongoEncryptionConverter` instance configured with `ClientEncryption` and a `EncryptionKeyResolver`. +3. A `PropertyValueConverterFactory` that uses the registered `MongoEncryptionConverter` bean. + +A side effect of using annotated key resolution is that the `@ExplicitEncrypted` annotation does not need to specify an alt key name. +The `EncryptionKeyResolver` uses an `EncryptionContext` providing access to the property allowing for dynamic DEK resolution. .Sample MongoEncryptionConverter Configuration ==== @@ -125,32 +133,38 @@ Since the `@ExplicitlyEncrypted` annotation does not need to specify an alt key ---- class Config extends AbstractMongoClientConfiguration { - // ... - @Autowired ApplicationContext appContext; - @Bean - MongoEncryptionConverter encryptingConverter() { + @Bean + ClientEncryption clientEncryption() { <1> + ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder(); + // … - ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder() - // ... + return ClientEncryptions.create(encryptionSettings); + } + + @Bean + MongoEncryptionConverter encryptingConverter(ClientEncryption clientEncryption) { - Encryption encryption = MongoClientEncryption.just(ClientEncryptions.create(encryptionSettings)) <1> - EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotationBased((ctx) -> ...); <2> + Encryption encryption = MongoClientEncryption.just(clientEncryption); + EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotated((ctx) -> …); <2> - return new MongoEncryptionConverter(encryption, keyResolver); <3> - } + return new MongoEncryptionConverter(encryption, keyResolver); <3> + } @Override protected void configureConverters(MongoConverterConfigurationAdapter adapter) { - adapter - .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(appContext)); <4> + adapter + .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(appContext)); <4> } } ---- -<1> Set up a `com.mongodb.client.vault.ClientEncryption` specific `Encryption` engine. -<2> Read the `EncryptionKey` from annotations on the field. + +<1> Set up a `Encryption` engine using `com.mongodb.client.vault.ClientEncryption`. +The instance is stateful and must be closed after usage. +Spring takes care of this because `ClientEncryption` is ``Closeable``. +<2> Set up an annotation-based `EncryptionKeyResolver` to determine the `EncryptionKey` from annotations. <3> Create the `MongoEncryptionConverter`. -<4> Enable for a `PropertyValueConverter` within the `BeanFactory`. +<4> Enable for a `PropertyValueConverter` lookup from the `BeanFactory`. ====