From 19211a0f8e8131491983286874a5c5ae65abbad7 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 5 Jul 2023 13:17:26 +0200 Subject: [PATCH] Fix decryption when client is using AutoEncryptionSettings#isBypassAutoEncryption(). This commit makes sure to convert already decrypted entries returned by the driver in case the client is configured with encryption settings. Closes #4432 Original pull request: #4439 --- .../encryption/MongoEncryptionConverter.java | 40 +- .../AbstractEncryptionTestBase.java | 733 ++++++++++++++++++ .../encryption/BypassAutoEncryptionTest.java | 100 +++ .../core/encryption/EncryptionTests.java | 608 +-------------- 4 files changed, 865 insertions(+), 616 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/AbstractEncryptionTestBase.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/BypassAutoEncryptionTest.java 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 bf7d87bef1..e0a741afe4 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 @@ -17,6 +17,7 @@ import java.util.Collection; import java.util.LinkedHashMap; +import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -25,6 +26,7 @@ import org.bson.BsonDocument; import org.bson.BsonValue; import org.bson.Document; +import org.bson.conversions.Bson; import org.bson.types.Binary; import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; @@ -63,7 +65,7 @@ public MongoEncryptionConverter(Encryption encryption, En public Object read(Object value, MongoConversionContext context) { Object decrypted = EncryptingConverter.super.read(value, context); - return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted; + return decrypted instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : decrypted; } @Override @@ -87,36 +89,56 @@ public Object decrypt(Object encryptedValue, EncryptionContext 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(), size); - iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); + iterable.forEach(it -> { + if(it instanceof BsonValue bsonValue) { + collection.add(BsonUtils.toJavaType(bsonValue)); + } else { + collection.add(context.read(it, persistentProperty.getActualType())); + } + }); + return collection; } else { Collection collection = CollectionFactory.createCollection(persistentProperty.getType(), size); iterable.forEach(it -> { - collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType())); + if(it instanceof BsonValue bsonValue) { + collection.add(context.read(BsonUtils.toJavaType(bsonValue), persistentProperty.getActualType())); + } else { + collection.add(context.read(it, persistentProperty.getActualType())); + } }); return collection; } } - if (!persistentProperty.isEntity() && decryptedValue instanceof BsonValue bsonValue) { - if (persistentProperty.isMap() && persistentProperty.getType() != Document.class) { - return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue)); - + if (!persistentProperty.isEntity() && persistentProperty.isMap()) { + if(persistentProperty.getType() != Document.class) { + if(decryptedValue instanceof BsonValue bsonValue) { + return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue)); + } + if(decryptedValue instanceof Document document) { + return new LinkedHashMap<>(document); + } + if(decryptedValue instanceof Map map) { + return map; + } } - return BsonUtils.toJavaType(bsonValue); } if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) { return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation().getType()); } + if (persistentProperty.isEntity() && decryptedValue instanceof Document document) { + return context.read(document, persistentProperty.getTypeInformation().getType()); + } + return decryptedValue; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/AbstractEncryptionTestBase.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/AbstractEncryptionTestBase.java new file mode 100644 index 0000000000..9360f2e785 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/AbstractEncryptionTestBase.java @@ -0,0 +1,733 @@ +/* + * 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.springframework.data.mongodb.core.EncryptionAlgorithms.*; +import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; + +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +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.vault.ClientEncryptions; +import org.assertj.core.api.Assertions; +import org.bson.BsonBinary; +import org.bson.Document; +import org.bson.types.Binary; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.dao.PermissionDeniedDataAccessException; +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.convert.encryption.MongoEncryptionConverter; +import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; +import org.springframework.data.mongodb.core.query.Update; + +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.vault.ClientEncryption; +import org.springframework.data.util.Lazy; + +/** + * @author Christoph Strobl + */ +public abstract class AbstractEncryptionTestBase { + + @Autowired MongoTemplate template; + + @Test // GH-4284 + void encryptAndDecryptSimpleValue() { + + Person source = new Person(); + source.id = "id-1"; + source.ssn = "mySecretSSN"; + + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } + + @Test // GH-4284 + void encryptAndDecryptComplexValue() { + + Person source = new Person(); + source.id = "id-1"; + source.address = new Address(); + source.address.city = "NYC"; + source.address.street = "4th Ave."; + + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } + + @Test // GH-4284 + void encryptAndDecryptValueWithinComplexOne() { + + 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 encryptAndDecryptListOfSimpleValue() { + + Person source = new Person(); + source.id = "id-1"; + source.listOfString = Arrays.asList("spring", "data", "mongodb"); + + template.save(source); + + verifyThat(source) // + .identifiedBy(Person::getId) // + .wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) // + .loadedIsEqualToSource(); + } + + @Test // GH-4284 + void encryptAndDecryptListOfComplexValue() { + + Person source = new Person(); + source.id = "id-1"; + + Address address = new Address(); + address.city = "SFO"; + address.street = "---"; + + 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 encryptAndDecryptMapOfSimpleValues() { + + 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 encryptAndDecryptMapOfComplexValues() { + + 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.save(source); + + template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value")) + .first(); + + 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 + 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 + void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException { + + BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("user-1"))); + + BsonBinary user2key = mongoClientEncryption.getClientEncryption().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; + }); + + // remove the key and invalidate encrypted data + mongoClientEncryption.getClientEncryption().deleteKey(user2key); + + // clear the 60 second key cache within the mongo client + mongoClientEncryption.destroy(); + + 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()); + } + + 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) { + AbstractEncryptionTestBase.this.assertSaved(source, idProvider, saved); + return this; + } + + SaveAndLoadAssert loadedMatches(Consumer expected) { + AbstractEncryptionTestBase.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 -> { + + MongoNamespace namespace = collection.getNamespace(); + + try (MongoClient rawClient = MongoClients.create()) { + return rawClient.getDatabase(namespace.getDatabaseName()).getCollection(namespace.getCollectionName()) + .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); + } + + protected static class EncryptionConfig 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 + MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) { + + Lazy dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); + + return new MongoEncryptionConverter(mongoClientEncryption, + EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get()))); + } + + @Bean + CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { + return new CachingMongoClientEncryption(() -> 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<>() { + { + put("local", new HashMap<>() { + { + put("key", localMasterKey); + } + }); + } + }; + + // Create the ClientEncryption instance + ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder() + .keyVaultMongoClientSettings( + MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) + .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); + } + } + } + + @org.springframework.data.mongodb.core.mapping.Document("test") + static class Person { + + String id; + String name; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // + String ssn; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "mySuperSecretKey") // + String wallet; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random + Address address; + + AddressWithEncryptedZip encryptedZip; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + List listOfString; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random + List
listOfComplex; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/name") // + String viaAltKeyNameField; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + Map mapOfString; + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // + Map mapOfComplex; + + public String getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public String getSsn() { + return this.ssn; + } + + public String getWallet() { + return this.wallet; + } + + public Address getAddress() { + return this.address; + } + + public AddressWithEncryptedZip getEncryptedZip() { + return this.encryptedZip; + } + + public List getListOfString() { + return this.listOfString; + } + + public List
getListOfComplex() { + return this.listOfComplex; + } + + public String getViaAltKeyNameField() { + return this.viaAltKeyNameField; + } + + public Map getMapOfString() { + return this.mapOfString; + } + + public Map getMapOfComplex() { + return this.mapOfComplex; + } + + public void setId(String id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public void setSsn(String ssn) { + this.ssn = ssn; + } + + public void setWallet(String wallet) { + this.wallet = wallet; + } + + public void setAddress(Address address) { + this.address = address; + } + + public void setEncryptedZip(AddressWithEncryptedZip encryptedZip) { + this.encryptedZip = encryptedZip; + } + + public void setListOfString(List listOfString) { + this.listOfString = listOfString; + } + + public void setListOfComplex(List
listOfComplex) { + this.listOfComplex = listOfComplex; + } + + public void setViaAltKeyNameField(String viaAltKeyNameField) { + this.viaAltKeyNameField = viaAltKeyNameField; + } + + public void setMapOfString(Map mapOfString) { + this.mapOfString = mapOfString; + } + + public void setMapOfComplex(Map mapOfComplex) { + this.mapOfComplex = mapOfComplex; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Person person = (Person) o; + return Objects.equals(id, person.id) && Objects.equals(name, person.name) && Objects.equals(ssn, person.ssn) + && Objects.equals(wallet, person.wallet) && Objects.equals(address, person.address) + && Objects.equals(encryptedZip, person.encryptedZip) && Objects.equals(listOfString, person.listOfString) + && Objects.equals(listOfComplex, person.listOfComplex) + && Objects.equals(viaAltKeyNameField, person.viaAltKeyNameField) + && Objects.equals(mapOfString, person.mapOfString) && Objects.equals(mapOfComplex, person.mapOfComplex); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, ssn, wallet, address, encryptedZip, listOfString, listOfComplex, viaAltKeyNameField, + mapOfString, mapOfComplex); + } + + public String toString() { + return "EncryptionTests.Person(id=" + this.getId() + ", name=" + this.getName() + ", ssn=" + this.getSsn() + + ", wallet=" + this.getWallet() + ", address=" + this.getAddress() + ", encryptedZip=" + + this.getEncryptedZip() + ", listOfString=" + this.getListOfString() + ", listOfComplex=" + + this.getListOfComplex() + ", viaAltKeyNameField=" + this.getViaAltKeyNameField() + ", mapOfString=" + + this.getMapOfString() + ", mapOfComplex=" + this.getMapOfComplex() + ")"; + } + } + + static class Address { + String city; + String street; + + public Address() {} + + public String getCity() { + return this.city; + } + + public String getStreet() { + return this.street; + } + + public void setCity(String city) { + this.city = city; + } + + public void setStreet(String street) { + this.street = street; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Address address = (Address) o; + return Objects.equals(city, address.city) && Objects.equals(street, address.street); + } + + @Override + public int hashCode() { + return Objects.hash(city, street); + } + + public String toString() { + return "EncryptionTests.Address(city=" + this.getCity() + ", street=" + this.getStreet() + ")"; + } + } + + static class AddressWithEncryptedZip extends Address { + + @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip; + + @Override + public String toString() { + return "AddressWithEncryptedZip{" + "zip='" + zip + '\'' + ", city='" + getCity() + '\'' + ", street='" + + getStreet() + '\'' + '}'; + } + + public String getZip() { + return this.zip; + } + + public void setZip(String zip) { + this.zip = zip; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/BypassAutoEncryptionTest.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/BypassAutoEncryptionTest.java new file mode 100644 index 0000000000..5ea600393e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/BypassAutoEncryptionTest.java @@ -0,0 +1,100 @@ +/* + * 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.junit.jupiter.api.Disabled; +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.data.convert.PropertyValueConverterFactory; +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.BypassAutoEncryptionTest.Config; +import org.springframework.data.util.Lazy; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.MongoClientSettings.Builder; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.model.vault.DataKeyOptions; +import com.mongodb.client.vault.ClientEncryptions; + +/** + * Encryption tests for client having {@link AutoEncryptionSettings#isBypassAutoEncryption()}. + * + * @author Christoph Strobl + */ +@ExtendWith(SpringExtension.class) +@ContextConfiguration(classes = Config.class) +public class BypassAutoEncryptionTest extends AbstractEncryptionTestBase { + + @Disabled + @Override + void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException { + super.altKeyDetection(mongoClientEncryption); + } + + @Configuration + static class Config extends EncryptionConfig { + + @Autowired ApplicationContext applicationContext; + + @Override + protected void configureClientSettings(Builder builder) { + + MongoClient mongoClient = MongoClients.create(); + ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(mongoClient); + mongoClient.close(); + + builder.autoEncryptionSettings( + AutoEncryptionSettings.builder().kmsProviders(clientEncryptionSettings.getKmsProviders()) + .keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()).bypassAutoEncryption(true).build()); + } + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) { + + converterConfigurationAdapter + .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext)); + } + + @Bean + MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) { + + Lazy dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local", + new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); + + return new MongoEncryptionConverter(mongoClientEncryption, + EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get()))); + } + + @Bean + CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { + return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings)); + } + + } + +} 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 f3c9fcf7ac..d790390d49 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 @@ -15,45 +15,23 @@ */ package org.springframework.data.mongodb.core.encryption; -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 java.security.SecureRandom; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.Objects; -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; 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.DisposableBean; 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.dao.PermissionDeniedDataAccessException; 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.convert.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config; -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; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -68,7 +46,6 @@ 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; /** @@ -76,345 +53,7 @@ */ @ExtendWith(SpringExtension.class) @ContextConfiguration(classes = Config.class) -public class EncryptionTests { - - @Autowired MongoTemplate template; - - @Test // GH-4284 - void encryptAndDecryptSimpleValue() { - - Person source = new Person(); - source.id = "id-1"; - source.ssn = "mySecretSSN"; - - template.save(source); - - verifyThat(source) // - .identifiedBy(Person::getId) // - .wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) // - .loadedIsEqualToSource(); - } - - @Test // GH-4284 - void encryptAndDecryptComplexValue() { - - Person source = new Person(); - source.id = "id-1"; - source.address = new Address(); - source.address.city = "NYC"; - source.address.street = "4th Ave."; - - template.save(source); - - verifyThat(source) // - .identifiedBy(Person::getId) // - .wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) // - .loadedIsEqualToSource(); - } - - @Test // GH-4284 - void encryptAndDecryptValueWithinComplexOne() { - - 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 encryptAndDecryptListOfSimpleValue() { - - Person source = new Person(); - source.id = "id-1"; - source.listOfString = Arrays.asList("spring", "data", "mongodb"); - - template.save(source); - - verifyThat(source) // - .identifiedBy(Person::getId) // - .wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) // - .loadedIsEqualToSource(); - } - - @Test // GH-4284 - void encryptAndDecryptListOfComplexValue() { - - Person source = new Person(); - source.id = "id-1"; - - Address address = new Address(); - address.city = "SFO"; - address.street = "---"; - - 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 encryptAndDecryptMapOfSimpleValues() { - - 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 encryptAndDecryptMapOfComplexValues() { - - 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.save(source); - - template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value")) - .first(); - - 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 - 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 - void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException { - - BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local", - new DataKeyOptions().keyAltNames(Collections.singletonList("user-1"))); - - BsonBinary user2key = mongoClientEncryption.getClientEncryption().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; - }); - - // remove the key and invalidate encrypted data - mongoClientEncryption.getClientEncryption().deleteKey(user2key); - - // clear the 60 second key cache within the mongo client - mongoClientEncryption.destroy(); - - 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()); - } - - 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); - } +public class EncryptionTests extends AbstractEncryptionTestBase { @Configuration static class Config extends AbstractMongoClientConfiguration { @@ -486,250 +125,5 @@ 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); - } - } - } - - @org.springframework.data.mongodb.core.mapping.Document("test") - static class Person { - - String id; - String name; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // - String ssn; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "mySuperSecretKey") // - String wallet; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random - Address address; - - AddressWithEncryptedZip encryptedZip; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random - List listOfString; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random - List
listOfComplex; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/name") // - String viaAltKeyNameField; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // - Map mapOfString; - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // - Map mapOfComplex; - - public String getId() { - return this.id; - } - - public String getName() { - return this.name; - } - - public String getSsn() { - return this.ssn; - } - - public String getWallet() { - return this.wallet; - } - - public Address getAddress() { - return this.address; - } - - public AddressWithEncryptedZip getEncryptedZip() { - return this.encryptedZip; - } - - public List getListOfString() { - return this.listOfString; - } - - public List
getListOfComplex() { - return this.listOfComplex; - } - - public String getViaAltKeyNameField() { - return this.viaAltKeyNameField; - } - - public Map getMapOfString() { - return this.mapOfString; - } - - public Map getMapOfComplex() { - return this.mapOfComplex; - } - - public void setId(String id) { - this.id = id; - } - - public void setName(String name) { - this.name = name; - } - - public void setSsn(String ssn) { - this.ssn = ssn; - } - - public void setWallet(String wallet) { - this.wallet = wallet; - } - - public void setAddress(Address address) { - this.address = address; - } - - public void setEncryptedZip(AddressWithEncryptedZip encryptedZip) { - this.encryptedZip = encryptedZip; - } - - public void setListOfString(List listOfString) { - this.listOfString = listOfString; - } - - public void setListOfComplex(List
listOfComplex) { - this.listOfComplex = listOfComplex; - } - - public void setViaAltKeyNameField(String viaAltKeyNameField) { - this.viaAltKeyNameField = viaAltKeyNameField; - } - - public void setMapOfString(Map mapOfString) { - this.mapOfString = mapOfString; - } - - public void setMapOfComplex(Map mapOfComplex) { - this.mapOfComplex = mapOfComplex; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Person person = (Person) o; - return Objects.equals(id, person.id) && Objects.equals(name, person.name) && Objects.equals(ssn, person.ssn) - && Objects.equals(wallet, person.wallet) && Objects.equals(address, person.address) - && Objects.equals(encryptedZip, person.encryptedZip) && Objects.equals(listOfString, person.listOfString) - && Objects.equals(listOfComplex, person.listOfComplex) - && Objects.equals(viaAltKeyNameField, person.viaAltKeyNameField) - && Objects.equals(mapOfString, person.mapOfString) && Objects.equals(mapOfComplex, person.mapOfComplex); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, ssn, wallet, address, encryptedZip, listOfString, listOfComplex, viaAltKeyNameField, - mapOfString, mapOfComplex); - } - - public String toString() { - return "EncryptionTests.Person(id=" + this.getId() + ", name=" + this.getName() + ", ssn=" + this.getSsn() - + ", wallet=" + this.getWallet() + ", address=" + this.getAddress() + ", encryptedZip=" - + this.getEncryptedZip() + ", listOfString=" + this.getListOfString() + ", listOfComplex=" - + this.getListOfComplex() + ", viaAltKeyNameField=" + this.getViaAltKeyNameField() + ", mapOfString=" - + this.getMapOfString() + ", mapOfComplex=" + this.getMapOfComplex() + ")"; - } - } - - static class Address { - String city; - String street; - - public Address() {} - - public String getCity() { - return this.city; - } - - public String getStreet() { - return this.street; - } - - public void setCity(String city) { - this.city = city; - } - - public void setStreet(String street) { - this.street = street; - } - - @Override - public boolean equals(Object o) { - if (o == this) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - Address address = (Address) o; - return Objects.equals(city, address.city) && Objects.equals(street, address.street); - } - - @Override - public int hashCode() { - return Objects.hash(city, street); - } - - public String toString() { - return "EncryptionTests.Address(city=" + this.getCity() + ", street=" + this.getStreet() + ")"; - } - } - - static class AddressWithEncryptedZip extends Address { - - @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip; - - @Override - public String toString() { - return "AddressWithEncryptedZip{" + "zip='" + zip + '\'' + ", city='" + getCity() + '\'' + ", street='" - + getStreet() + '\'' + '}'; - } - - public String getZip() { - return this.zip; - } - - public void setZip(String zip) { - this.zip = zip; - } } }