Browse Source

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.

Original pull request: #4302
See: #4284
pull/4309/merge
Mark Paluch 3 years ago
parent
commit
67215f1209
No known key found for this signature in database
GPG Key ID: 4406B84C1661DCD1
  1. 27
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java
  2. 15
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java
  3. 14
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java
  4. 86
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java
  5. 7
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java
  6. 2
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java
  7. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java
  8. 114
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKey.java
  9. 42
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java
  10. 23
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java
  11. 54
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyAltName.java
  12. 59
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/KeyId.java
  13. 41
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java
  14. 6
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java
  15. 19
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java
  16. 32
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java
  17. 10
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java
  18. 82
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java
  19. 18
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java
  20. 32
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java
  21. 106
      src/main/asciidoc/reference/mongo-encryption.adoc

27
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

@ -17,16 +17,7 @@ package org.springframework.data.mongodb.core.convert;
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.*;
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.function.Predicate; import java.util.function.Predicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -39,7 +30,6 @@ import org.bson.codecs.configuration.CodecRegistry;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.bson.json.JsonReader; import org.bson.json.JsonReader;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
@ -51,16 +41,7 @@ import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Reference; import org.springframework.data.annotation.Reference;
import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.CustomConversions;
import org.springframework.data.convert.TypeMapper; import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.AccessOptions; import org.springframework.data.mapping.*;
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.callback.EntityCallbacks; import org.springframework.data.mapping.callback.EntityCallbacks;
import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor; import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
@ -902,7 +883,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversions.hasValueConverter(prop)) { if (conversions.hasValueConverter(prop)) {
accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj, accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj,
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() { new MongoConversionContext(new PropertyValueProvider<>() {
@Nullable @Nullable
@Override @Override
public <T> T getPropertyValue(MongoPersistentProperty property) { public <T> T getPropertyValue(MongoPersistentProperty property) {
@ -1245,7 +1226,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
if (conversions.hasValueConverter(property)) { if (conversions.hasValueConverter(property)) {
accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value, accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value,
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() { new MongoConversionContext(new PropertyValueProvider<>() {
@Nullable @Nullable
@Override @Override
public <T> T getPropertyValue(MongoPersistentProperty property) { public <T> T getPropertyValue(MongoPersistentProperty property) {

15
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; package org.springframework.data.mongodb.core.convert;
import java.util.function.Supplier;
import org.bson.conversions.Bson; import org.bson.conversions.Bson;
import org.springframework.data.convert.ValueConversionContext; 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.PropertyValueProvider;
import org.springframework.data.mapping.model.SpELContext; 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.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
@ -36,18 +31,20 @@ import org.springframework.lang.Nullable;
*/ */
public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> { public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
private final PropertyValueProvider accessor; // TODO: generics private final PropertyValueProvider<MongoPersistentProperty> accessor; // TODO: generics
private final MongoPersistentProperty persistentProperty; private final MongoPersistentProperty persistentProperty;
private final MongoConverter mongoConverter; private final MongoConverter mongoConverter;
@Nullable @Nullable
private final SpELContext spELContext; private final SpELContext spELContext;
public MongoConversionContext(PropertyValueProvider<?> accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
this(accessor, persistentProperty, mongoConverter, null); this(accessor, persistentProperty, mongoConverter, null);
} }
public MongoConversionContext(PropertyValueProvider<?> accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, SpELContext spELContext) { public MongoConversionContext(PropertyValueProvider<MongoPersistentProperty> accessor,
MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, @Nullable SpELContext spELContext) {
this.accessor = accessor; this.accessor = accessor;
this.persistentProperty = persistentProperty; this.persistentProperty = persistentProperty;
@ -60,11 +57,13 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
return persistentProperty; return persistentProperty;
} }
@Nullable
public Object getValue(String propertyPath) { public Object getValue(String propertyPath) {
return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath)); return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath));
} }
@Override @Override
@SuppressWarnings("unchecked")
public <T> T write(@Nullable Object value, TypeInformation<T> target) { public <T> T write(@Nullable Object value, TypeInformation<T> target) {
return (T) mongoConverter.convertToMongoType(value, target); return (T) mongoConverter.convertToMongoType(value, target);
} }

14
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.convert.MongoValueConverter;
import org.springframework.data.mongodb.core.encryption.EncryptionContext; 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 * @author Christoph Strobl
* @since 4.1 * @since 4.1
@ -32,20 +32,20 @@ public interface EncryptingConverter<S, T> extends MongoValueConverter<S, T> {
return decrypt(value, buildEncryptionContext(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}. * Decrypt the given encrypted source value within the given {@link EncryptionContext context}.
* *
* @param encryptedValue the encrypted source. * @param encryptedValue the encrypted source.
* @param context the context to operate in. * @param context the context to operate in.
* @return never {@literal null}. * @return never {@literal null}.
*/ */
S decrypt(Object encryptedValue, EncryptionContext context); 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}. * Encrypt the given raw source value within the given {@link EncryptionContext context}.
* *

86
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java

@ -39,6 +39,9 @@ import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils; 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 * @author Christoph Strobl
* @since 4.1 * @since 4.1
*/ */
@ -46,7 +49,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class); private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class);
private Encryption<BsonValue, BsonBinary> encryption; private final Encryption<BsonValue, BsonBinary> encryption;
private final EncryptionKeyResolver keyResolver; private final EncryptionKeyResolver keyResolver;
public MongoEncryptionConverter(Encryption<BsonValue, BsonBinary> encryption, EncryptionKeyResolver keyResolver) { public MongoEncryptionConverter(Encryption<BsonValue, BsonBinary> encryption, EncryptionKeyResolver keyResolver) {
@ -70,7 +73,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) { if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) {
if (LOGGER.isDebugEnabled()) { 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())); getProperty(context).getName()));
} }
@ -83,18 +86,20 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
} }
} }
MongoPersistentProperty persistentProperty = getProperty(context); MongoPersistentProperty persistentProperty = getProperty(context);
if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable<?> iterable) { if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable<?> iterable) {
int size = iterable instanceof Collection<?> c ? c.size() : 10;
if (!persistentProperty.isEntity()) { if (!persistentProperty.isEntity()) {
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it))); iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it)));
return collection; return collection;
} else { } else {
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), 10); Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
iterable.forEach(it -> { iterable.forEach(it -> {
collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType()));
persistentProperty.getActualType()));
}); });
return collection; return collection;
} }
@ -109,17 +114,12 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
} }
if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) { if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) {
return context.read(BsonUtils.toJavaType(bsonDocument), return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation().getType());
persistentProperty.getTypeInformation().getType());
} }
return decryptedValue; return decryptedValue;
} }
private MongoPersistentProperty getProperty(EncryptionContext context) {
return context.getProperty();
}
@Override @Override
public Object encrypt(Object value, EncryptionContext context) { public Object encrypt(Object value, EncryptionContext context) {
@ -128,15 +128,19 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
getProperty(context).getName())); getProperty(context).getName()));
} }
MongoPersistentProperty persistentProperty = getProperty(context); MongoPersistentProperty persistentProperty = getProperty(context);
Encrypted annotation = persistentProperty.findAnnotation(Encrypted.class); Encrypted annotation = persistentProperty.findAnnotation(Encrypted.class);
if(annotation == null) { if (annotation == null) {
annotation = persistentProperty.getOwner().findAnnotation(Encrypted.class); annotation = persistentProperty.getOwner().findAnnotation(Encrypted.class);
} }
EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm()); if (annotation == null) {
encryptionOptions.setKey(keyResolver.getKey(context)); 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()) { if (!persistentProperty.isEntity()) {
@ -162,36 +166,44 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions); return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions);
} }
public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,
EncryptionContext context) { EncryptionContext context) {
BsonArray bsonArray = new BsonArray(); BsonArray bsonArray = new BsonArray();
if (!property.isEntity()) { boolean isEntity = property.isEntity();
if (value instanceof Collection values) {
values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it))); if (value instanceof Collection<?> values) {
} else if (ObjectUtils.isArray(value)) { values.forEach(it -> {
for (Object o : ObjectUtils.toObjectArray(value)) {
bsonArray.add(BsonUtils.simpleToBsonValue(o)); 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 (ObjectUtils.isArray(value)) {
} else {
if (value instanceof Collection values) { for (Object o : ObjectUtils.toObjectArray(value)) {
values.forEach(it -> {
Document write = (Document) context.write(it, property.getTypeInformation()); if (isEntity) {
bsonArray.add(write.toBsonDocument()); Document document = (Document) context.write(o, property.getTypeInformation());
}); bsonArray.add(document == null ? null : document.toBsonDocument());
} else if (ObjectUtils.isArray(value)) { } else {
for (Object o : ObjectUtils.toObjectArray(value)) { bsonArray.add(BsonUtils.simpleToBsonValue(o));
Document write = (Document) context.write(o, property.getTypeInformation());
bsonArray.add(write.toBsonDocument());
} }
} }
return bsonArray;
} }
return bsonArray;
} }
@Override
public EncryptionContext buildEncryptionContext(MongoConversionContext context) { public EncryptionContext buildEncryptionContext(MongoConversionContext context) {
return new ExplicitEncryptionContext(context); return new ExplicitEncryptionContext(context);
} }
protected MongoPersistentProperty getProperty(EncryptionContext context) {
return context.getProperty();
}
} }

7
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java

@ -0,0 +1,7 @@
/**
* Converters integrating with
* <a href="https://www.mongodb.com/docs/manual/core/csfle/fundamentals/manual-encryption/">explicit encryption
* mechanism of Client-Side Field Level Encryption</a>.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.mongodb.core.convert.encryption;

2
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; package org.springframework.data.mongodb.core.encryption;
/** /**
* Component responsible for en-/decrypting values. * Component responsible for encrypting and decrypting values.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @since 4.1 * @since 4.1

6
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; package org.springframework.data.mongodb.core.encryption;
import org.springframework.data.convert.ValueConversionContext;
import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
@ -23,7 +22,10 @@ import org.springframework.expression.EvaluationContext;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* Context to encapsulate encryption for a specific {@link MongoPersistentProperty}.
*
* @author Christoph Strobl * @author Christoph Strobl
* @since 4.1
*/ */
public interface EncryptionContext { public interface EncryptionContext {
@ -36,7 +38,7 @@ public interface EncryptionContext {
/** /**
* Shortcut for converting a given {@literal value} into its store representation using the root * Shortcut for converting a given {@literal value} into its store representation using the root
* {@link ValueConversionContext}. * {@code ValueConversionContext}.
* *
* @param value * @param value
* @return * @return

114
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; package org.springframework.data.mongodb.core.encryption;
import org.bson.BsonBinary; import org.bson.BsonBinary;
import org.bson.BsonBinarySubType; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/** /**
* The {@link EncryptionKey} represents a {@literal Data Encryption Key} reference that can be either direct via the * 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 * @author Christoph Strobl
* @since 4.1 * @since 4.1
*/ */
public interface EncryptionKey { 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. * Create a new {@link EncryptionKey} that uses the keys id for reference.
* *
* @param key must not be {@literal null}. * @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); return new KeyId(key);
} }
/** /**
* Create a new {@link EncryptionKey} that uses an {@literal Key Alternative Name} for reference. * Create a new {@link EncryptionKey} that uses an {@literal Key Alternative Name} for reference.
* *
* @param altKeyName must not be {@literal null}. * @param keyAltName must not be {@literal null} or empty.
* @return new instance of {@link KeyId}. * @return new instance of {@link EncryptionKey KeyAltName}.
*/
static AltKeyName altKeyName(String altKeyName) {
return new AltKeyName(altKeyName);
}
/**
* @param value must not be {@literal null}.
*/ */
record KeyId(BsonBinary value) implements EncryptionKey { static EncryptionKey keyAltName(String keyAltName) {
@Override
public Type type() {
return Type.ID;
}
@Override Assert.hasText(keyAltName, "Key Alternative Name must not be empty");
public String toString() {
if (BsonBinarySubType.isUuid(value.getType())) { return new KeyAltName(keyAltName);
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);
}
} }
/** /**
* @param value must not be {@literal null}. * @return the value that allows to reference a specific key.
*/ */
record AltKeyName(String value) implements EncryptionKey { Object value();
@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);
}
@Override /**
public int hashCode() { * @return the {@link Type} of reference.
return ObjectUtils.nullSafeHashCode(value); */
} Type type();
}
/** /**
* The key reference type. * The key reference type.

42
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolver.java

@ -18,18 +18,20 @@ package org.springframework.data.mongodb.core.encryption;
import org.bson.BsonBinary; import org.bson.BsonBinary;
import org.bson.types.Binary; import org.bson.types.Binary;
import org.springframework.data.mongodb.core.mapping.Encrypted; 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.BsonUtils;
import org.springframework.data.mongodb.util.encryption.EncryptionUtils; import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
/** /**
* Interface to obtain a {@link EncryptionKey Data Encryption Key} that is valid in a given {@link EncryptionContext * Interface to obtain a {@link EncryptionKey Data Encryption Key} that is valid in a given {@link EncryptionContext
* context}. * context}.
* <p> * <p>
* Use the {@link #annotationBased(EncryptionKeyResolver) based} variant which will first try to resolve a potential * Use the {@link #annotated(EncryptionKeyResolver) based} variant which will first try to resolve a potential
* {@link ExplicitlyEncrypted#altKeyName() Key Alternate Name} from annotations before calling the fallback resolver. * {@link ExplicitEncrypted#keyAltName() Key Alternate Name} from annotations before calling the fallback resolver.
* *
* @author Christoph Strobl * @author Christoph Strobl
* @since 4.1 * @since 4.1
* @see EncryptionKey * @see EncryptionKey
@ -46,20 +48,23 @@ public interface EncryptionKeyResolver {
EncryptionKey getKey(EncryptionContext encryptionContext); 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. * fallback {@link EncryptionKeyResolver resolver} if no {@literal Key Alternate Name} is present.
* *
* @param fallback must not be {@literal null}. * @param fallback must not be {@literal null}.
* @return new instance of {@link EncryptionKeyResolver}. * @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) -> { return ((encryptionContext) -> {
ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class); MongoPersistentProperty property = encryptionContext.getProperty();
if (annotation == null || !StringUtils.hasText(annotation.altKeyName())) { 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) { if (encrypted == null) {
return fallback.getKey(encryptionContext); return fallback.getKey(encryptionContext);
} }
@ -73,19 +78,22 @@ public interface EncryptionKeyResolver {
return EncryptionKey.keyId((BsonBinary) BsonUtils.simpleToBsonValue(binary)); return EncryptionKey.keyId((BsonBinary) BsonUtils.simpleToBsonValue(binary));
} }
if (o instanceof String string) { 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(); String keyAltName = annotation.keyAltName();
if (altKeyName.startsWith("/")) { if (keyAltName.startsWith("/")) {
Object fieldValue = encryptionContext.lookupValue(altKeyName.replace("/", "")); Object fieldValue = encryptionContext.lookupValue(keyAltName.replace("/", ""));
if (fieldValue == null) { 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 { } else {
return new EncryptionKey.AltKeyName(altKeyName); return new KeyAltName(keyAltName);
} }
}); });
} }

23
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; package org.springframework.data.mongodb.core.encryption;
import org.springframework.lang.Nullable; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
@ -27,16 +27,15 @@ import org.springframework.util.ObjectUtils;
public class EncryptionOptions { public class EncryptionOptions {
private final String algorithm; private final String algorithm;
private @Nullable EncryptionKey key; private final EncryptionKey key;
public EncryptionOptions(String algorithm) { public EncryptionOptions(String algorithm, EncryptionKey key) {
this.algorithm = algorithm;
}
public EncryptionOptions setKey(EncryptionKey key) { Assert.hasText(algorithm, "Algorithm must not be empty");
Assert.notNull(key, "EncryptionKey must not be empty");
this.key = key; this.key = key;
return this; this.algorithm = algorithm;
} }
public EncryptionKey key() { public EncryptionKey key() {
@ -47,11 +46,6 @@ public class EncryptionOptions {
return algorithm; return algorithm;
} }
@Override
public String toString() {
return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
}
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
@ -77,4 +71,9 @@ public class EncryptionOptions {
result = 31 * result + ObjectUtils.nullSafeHashCode(key); result = 31 * result + ObjectUtils.nullSafeHashCode(key);
return result; return result;
} }
@Override
public String toString() {
return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
}
} }

54
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);
}
}

59
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);
}
}

41
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; package org.springframework.data.mongodb.core.encryption;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier; import java.util.function.Supplier;
import org.bson.BsonBinary; import org.bson.BsonBinary;
@ -35,23 +34,9 @@ import com.mongodb.client.vault.ClientEncryption;
public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> { public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> {
private final Supplier<ClientEncryption> source; private final Supplier<ClientEncryption> source;
private final AtomicReference<ClientEncryption> cached;
private MongoClientEncryption(Supplier<ClientEncryption> source) {
MongoClientEncryption(Supplier<ClientEncryption> source) {
this.source = 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> clientEncryption) {
return new MongoClientEncryption(clientEncryption);
} }
/** /**
@ -64,27 +49,7 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
Assert.notNull(clientEncryption, "ClientEncryption must not be null"); Assert.notNull(clientEncryption, "ClientEncryption must not be null");
return new MongoClientEncryption(() -> clientEncryption) { 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();
} }
@Override @Override
@ -107,7 +72,7 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
} }
public ClientEncryption getClientEncryption() { public ClientEncryption getClientEncryption() {
return cached.get(); return source.get();
} }
} }

6
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java

@ -0,0 +1,6 @@
/**
* Infrastructure for <a href="https://www.mongodb.com/docs/manual/core/csfle/fundamentals/manual-encryption/">explicit
* encryption mechanism of Client-Side Field Level Encryption</a>.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.mongodb.core.encryption;

19
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitlyEncrypted.java → 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.EncryptingConver
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
/** /**
* {@link ExplicitlyEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that * {@link ExplicitEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that indicates
* indicates the target element is subject to encryption during the mapping process, in which a given domain type is * the target element is subject to encryption during the mapping process, in which a given domain type is converted
* converted into the store specific format. * into the store specific format.
* <p> * <p>
* The {@link #value()} attribute, defines the bean type to look up within the * 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 * {@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. * of the encryption process.
* *
* <pre class="code"> * <pre class="code">
@ -41,11 +41,11 @@ import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionC
* private ObjectId id; * private ObjectId id;
* private String name; * private String name;
* *
* &#64;ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "secred-key-alternative-name") // * &#64;ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "secred-key-alternative-name") //
* private String ssn; * private String ssn;
* } * }
* </pre> * </pre>
* *
* @author Christoph Strobl * @author Christoph Strobl
* @since 4.1 * @since 4.1
* @see ValueConverter * @see ValueConverter
@ -54,7 +54,7 @@ import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionC
@Target(ElementType.FIELD) @Target(ElementType.FIELD)
@Encrypted @Encrypted
@ValueConverter @ValueConverter
public @interface ExplicitlyEncrypted { public @interface ExplicitEncrypted {
/** /**
* Define the algorithm to use. * Define the algorithm to use.
@ -66,6 +66,7 @@ public @interface ExplicitlyEncrypted {
* objects and arrays as well as the query limitations that come with each of them. * 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. * @return the string representation of the encryption algorithm to use.
* @see org.springframework.data.mongodb.core.EncryptionAlgorithms
*/ */
@AliasFor(annotation = Encrypted.class, value = "algorithm") @AliasFor(annotation = Encrypted.class, value = "algorithm")
String algorithm() default ""; String algorithm() default "";
@ -78,10 +79,10 @@ public @interface ExplicitlyEncrypted {
* It is possible to use the {@literal "/"} character as a prefix to access a particular field value in the same * 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 * 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. * update operations will require the full object to resolve those values.
* *
* @return the {@literal Key Alternate Name} if set or an empty {@link String}. * @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. * The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property.

32
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyResolverUnitTests.java

@ -35,11 +35,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.springframework.data.mongodb.core.mapping.Encrypted; 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.data.mongodb.test.util.MongoTestMappingContext;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
/** /**
* Unit tests for {@link EncryptionKeyResolver}.
*
* @author Christoph Strobl * @author Christoph Strobl
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -65,7 +67,7 @@ class EncryptionKeyResolverUnitTests {
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
AnnotatedWithExplicitlyEncrypted::getNotAnnotated); AnnotatedWithExplicitlyEncrypted::getNotAnnotated);
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
assertThat(key).isSameAs(defaultEncryptionKey); assertThat(key).isSameAs(defaultEncryptionKey);
} }
@ -76,7 +78,7 @@ class EncryptionKeyResolverUnitTests {
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
AnnotatedWithExplicitlyEncrypted::getAlgorithm); AnnotatedWithExplicitlyEncrypted::getAlgorithm);
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
assertThat(key).isSameAs(defaultEncryptionKey); assertThat(key).isSameAs(defaultEncryptionKey);
} }
@ -87,9 +89,9 @@ class EncryptionKeyResolverUnitTests {
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class, EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyName); 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 @Test // GH-4284
@ -99,9 +101,9 @@ class EncryptionKeyResolverUnitTests {
AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyNameFromPropertyValue); AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyNameFromPropertyValue);
when(ctx.lookupValue(eq("notAnnotated"))).thenReturn("born-to-be-wild"); 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 @Test // GH-4284
@ -111,7 +113,7 @@ class EncryptionKeyResolverUnitTests {
AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType.class, AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType.class,
AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType::getKeyIdFromDomainType); AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType::getKeyIdFromDomainType);
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx); EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
assertThat(key).isEqualTo(EncryptionKey.keyId( assertThat(key).isEqualTo(EncryptionKey.keyId(
new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g==")))); new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g=="))));
@ -127,7 +129,7 @@ class EncryptionKeyResolverUnitTests {
when(ctx.getEvaluationContext(any())).thenReturn(evaluationContext); 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( assertThat(key).isEqualTo(EncryptionKey.keyId(
new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g==")))); new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g=="))));
@ -145,13 +147,13 @@ class EncryptionKeyResolverUnitTests {
String notAnnotated; String notAnnotated;
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
String algorithm; 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; 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; String algorithmAndAltKeyNameFromPropertyValue;
} }
@ -159,10 +161,10 @@ class EncryptionKeyResolverUnitTests {
@Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==") @Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==")
class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType { class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType {
@ExplicitlyEncrypted // @ExplicitEncrypted //
String keyIdFromDomainType; String keyIdFromDomainType;
@ExplicitlyEncrypted(altKeyName = "sec-key-name") // @ExplicitEncrypted(keyAltName = "sec-key-name") //
String altKeyNameFromPropertyIgnoringKeyIdFromDomainType; String altKeyNameFromPropertyIgnoringKeyIdFromDomainType;
} }
@ -170,7 +172,7 @@ class EncryptionKeyResolverUnitTests {
@Encrypted(keyId = "#{#myKeyId}") @Encrypted(keyId = "#{#myKeyId}")
class KeyIdFromSpel { class KeyIdFromSpel {
@ExplicitlyEncrypted // @ExplicitEncrypted //
String keyIdFromDomainType; String keyIdFromDomainType;
} }
} }

10
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionKeyUnitTests.java

@ -24,6 +24,8 @@ import org.bson.UuidRepresentation;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** /**
* Unit tests for {@link EncryptionKey}.
*
* @author Christoph Strobl * @author Christoph Strobl
*/ */
class EncryptionKeyUnitTests { class EncryptionKeyUnitTests {
@ -40,9 +42,9 @@ class EncryptionKeyUnitTests {
@Test // GH-4284 @Test // GH-4284
void altKeyNameToStringDoesNotRevealEntireKey() { void altKeyNameToStringDoesNotRevealEntireKey() {
assertThat(EncryptionKey.altKeyName("s").toString()).contains("***"); assertThat(EncryptionKey.keyAltName("s").toString()).contains("***");
assertThat(EncryptionKey.altKeyName("su").toString()).contains("***"); assertThat(EncryptionKey.keyAltName("su").toString()).contains("***");
assertThat(EncryptionKey.altKeyName("sup").toString()).contains("***"); assertThat(EncryptionKey.keyAltName("sup").toString()).contains("***");
assertThat(EncryptionKey.altKeyName("super-secret-key").toString()).contains("sup***"); assertThat(EncryptionKey.keyAltName("super-secret-key").toString()).contains("sup***");
} }
} }

82
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/EncryptionTests.java

@ -30,8 +30,10 @@ import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.function.Supplier;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.bson.BsonBinary; import org.bson.BsonBinary;
@ -39,6 +41,7 @@ import org.bson.Document;
import org.bson.types.Binary; import org.bson.types.Binary;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -52,7 +55,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config; 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.mongodb.core.query.Update;
import org.springframework.data.util.Lazy; import org.springframework.data.util.Lazy;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
@ -68,6 +71,7 @@ import com.mongodb.client.model.Filters;
import com.mongodb.client.model.IndexOptions; import com.mongodb.client.model.IndexOptions;
import com.mongodb.client.model.Indexes; import com.mongodb.client.model.Indexes;
import com.mongodb.client.model.vault.DataKeyOptions; import com.mongodb.client.model.vault.DataKeyOptions;
import com.mongodb.client.vault.ClientEncryption;
import com.mongodb.client.vault.ClientEncryptions; import com.mongodb.client.vault.ClientEncryptions;
/** /**
@ -80,7 +84,7 @@ public class EncryptionTests {
@Autowired MongoTemplate template; @Autowired MongoTemplate template;
@Test // GH-4284 @Test // GH-4284
void enDeCryptSimpleValue() { void encryptAndDecryptSimpleValue() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
@ -95,7 +99,7 @@ public class EncryptionTests {
} }
@Test // GH-4284 @Test // GH-4284
void enDeCryptComplexValue() { void encryptAndDecryptComplexValue() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
@ -112,7 +116,7 @@ public class EncryptionTests {
} }
@Test // GH-4284 @Test // GH-4284
void enDeCryptValueWithinComplexOne() { void encryptAndDecryptValueWithinComplexOne() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
@ -135,7 +139,7 @@ public class EncryptionTests {
} }
@Test // GH-4284 @Test // GH-4284
void enDeCryptListOfSimpleValue() { void encryptAndDecryptListOfSimpleValue() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
@ -150,7 +154,7 @@ public class EncryptionTests {
} }
@Test // GH-4284 @Test // GH-4284
void enDeCryptListOfComplexValue() { void encryptAndDecryptListOfComplexValue() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
@ -170,7 +174,7 @@ public class EncryptionTests {
} }
@Test // GH-4284 @Test // GH-4284
void enDeCryptMapOfSimpleValues() { void encryptAndDecryptMapOfSimpleValues() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
@ -185,7 +189,7 @@ public class EncryptionTests {
} }
@Test // GH-4284 @Test // GH-4284
void enDeCryptMapOfComplexValues() { void encryptAndDecryptMapOfComplexValues() {
Person source = new Person(); Person source = new Person();
source.id = "id-1"; source.id = "id-1";
@ -312,7 +316,7 @@ public class EncryptionTests {
} }
@Test @Test
void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) throws InterruptedException { void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local", BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local",
new DataKeyOptions().keyAltNames(Collections.singletonList("user-1"))); new DataKeyOptions().keyAltNames(Collections.singletonList("user-1")));
@ -349,7 +353,7 @@ public class EncryptionTests {
mongoClientEncryption.getClientEncryption().deleteKey(user2key); mongoClientEncryption.getClientEncryption().deleteKey(user2key);
// clear the 60 second key cache within the mongo client // 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); assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1);
@ -444,12 +448,12 @@ public class EncryptionTests {
new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey")))); new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))));
return new MongoEncryptionConverter(mongoClientEncryption, return new MongoEncryptionConverter(mongoClientEncryption,
EncryptionKeyResolver.annotationBased((ctx) -> EncryptionKey.keyId(dataKey.get()))); EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get())));
} }
@Bean @Bean
MongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
return MongoClientEncryption.caching(() -> ClientEncryptions.create(encryptionSettings)); return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
} }
@Bean @Bean
@ -468,9 +472,9 @@ public class EncryptionTests {
final byte[] localMasterKey = new byte[96]; final byte[] localMasterKey = new byte[96];
new SecureRandom().nextBytes(localMasterKey); new SecureRandom().nextBytes(localMasterKey);
Map<String, Map<String, Object>> kmsProviders = new HashMap<String, Map<String, Object>>() { Map<String, Map<String, Object>> kmsProviders = new HashMap<>() {
{ {
put("local", new HashMap<String, Object>() { put("local", new HashMap<>() {
{ {
put("key", localMasterKey); put("key", localMasterKey);
} }
@ -485,6 +489,36 @@ public class EncryptionTests {
.keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build(); .keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build();
return clientEncryptionSettings; return clientEncryptionSettings;
} }
}
static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean {
static final AtomicReference<ClientEncryption> cache = new AtomicReference<>();
CachingMongoClientEncryption(Supplier<ClientEncryption> 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 @Data
@ -494,30 +528,30 @@ public class EncryptionTests {
String id; String id;
String name; String name;
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
String ssn; 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; 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; Address address;
AddressWithEncryptedZip encryptedZip; 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<String> listOfString; List<String> 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<Address> listOfComplex; List<Address> 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; String viaAltKeyNameField;
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
Map<String, String> mapOfString; Map<String, String> mapOfString;
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
Map<String, Address> mapOfComplex; Map<String, Address> mapOfComplex;
} }
@ -531,7 +565,7 @@ public class EncryptionTests {
@Setter @Setter
static class AddressWithEncryptedZip extends Address { 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 @Override
public String toString() { public String toString() {

18
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryptionUnitTests.java

@ -33,6 +33,8 @@ import com.mongodb.client.model.vault.EncryptOptions;
import com.mongodb.client.vault.ClientEncryption; import com.mongodb.client.vault.ClientEncryption;
/** /**
* Unit tests for {@link MongoClientEncryption}.
*
* @author Christoph Strobl * @author Christoph Strobl
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
@ -55,7 +57,7 @@ class MongoClientEncryptionUnitTests {
MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption); MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption);
mce.encrypt(new BsonBinary(new byte[0]), 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<EncryptOptions> options = ArgumentCaptor.forClass(EncryptOptions.class); ArgumentCaptor<EncryptOptions> options = ArgumentCaptor.forClass(EncryptOptions.class);
verify(clientEncryption).encrypt(any(), options.capture()); verify(clientEncryption).encrypt(any(), options.capture());
@ -63,23 +65,12 @@ class MongoClientEncryptionUnitTests {
assertThat(options.getValue().getKeyAltName()).isEqualTo("sec-key-name"); 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 @Test // GH-4284
void refreshObtainsNextInstanceFromSupplier() { void refreshObtainsNextInstanceFromSupplier() {
ClientEncryption next = mock(ClientEncryption.class); ClientEncryption next = mock(ClientEncryption.class);
MongoClientEncryption mce = MongoClientEncryption.caching(new Supplier<>() { MongoClientEncryption mce = new MongoClientEncryption(new Supplier<>() {
int counter = 0; int counter = 0;
@ -90,7 +81,6 @@ class MongoClientEncryptionUnitTests {
}); });
assertThat(mce.getClientEncryption()).isSameAs(clientEncryption); assertThat(mce.getClientEncryption()).isSameAs(clientEncryption);
assertThat(mce.refresh()).isTrue();
assertThat(mce.getClientEncryption()).isSameAs(next); assertThat(mce.getClientEncryption()).isSameAs(next);
} }
} }

32
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoEncryptionConverterUnitTests.java

@ -41,7 +41,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness; import org.mockito.quality.Strictness;
import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoConversionContext;
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; 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.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.test.util.MongoTestMappingContext; import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
@ -72,10 +72,10 @@ class MongoEncryptionConverterUnitTests {
@BeforeEach @BeforeEach
void 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())) when(encryption.encrypt(valueToBeEncrypted.capture(), encryptionOptions.capture()))
.thenReturn(new BsonBinary(new byte[0])); .thenReturn(new BsonBinary(new byte[0]));
keyResolver = EncryptionKeyResolver.annotationBased(fallbackKeyResolver); keyResolver = EncryptionKeyResolver.annotated(fallbackKeyResolver);
converter = new MongoEncryptionConverter(encryption, keyResolver); converter = new MongoEncryptionConverter(encryption, keyResolver);
} }
@ -89,7 +89,7 @@ class MongoEncryptionConverterUnitTests {
assertThat(valueToBeEncrypted.getValue()).isEqualTo(new BsonString("foo")); assertThat(valueToBeEncrypted.getValue()).isEqualTo(new BsonString("foo"));
assertThat(encryptionOptions.getValue()).isEqualTo( 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 @Test // GH-4284
@ -101,7 +101,7 @@ class MongoEncryptionConverterUnitTests {
converter.write("foo", conversionContext); converter.write("foo", conversionContext);
assertThat(encryptionOptions.getValue()).isEqualTo( 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 @Test // GH-4284
@ -117,7 +117,7 @@ class MongoEncryptionConverterUnitTests {
assertThat(path.getValue()).isEqualTo("notAnnotated"); assertThat(path.getValue()).isEqualTo("notAnnotated");
assertThat(encryptionOptions.getValue()) 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 @Test // GH-4284
@ -216,33 +216,33 @@ class MongoEncryptionConverterUnitTests {
String notAnnotated; String notAnnotated;
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) // @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
String stringValueWithAlgorithmOnly; 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; 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; 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; JustATypeWithAnUnencryptedField nestedFullyEncrypted;
NestedWithEncryptedField nestedWithEncryptedField; NestedWithEncryptedField nestedWithEncryptedField;
// Client-Side Field Level Encryption does not support encrypting individual array elements // 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<String> listOfString; List<String> listOfString;
// Client-Side Field Level Encryption does not support encrypting individual array elements // 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<JustATypeWithAnUnencryptedField> listOfComplex; List<JustATypeWithAnUnencryptedField> listOfComplex;
// just as it was a domain type encrypt the entire thing here // 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<String, String> mapOfString; Map<String, String> mapOfString;
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
Map<String, JustATypeWithAnUnencryptedField> mapOfComplex; Map<String, JustATypeWithAnUnencryptedField> mapOfComplex;
RecordWithEncryptedValue recordWithEncryptedValue; RecordWithEncryptedValue recordWithEncryptedValue;
@ -257,10 +257,10 @@ class MongoEncryptionConverterUnitTests {
static class NestedWithEncryptedField extends 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; String encryptedValue;
} }
record RecordWithEncryptedValue(@ExplicitlyEncrypted String value) { record RecordWithEncryptedValue(@ExplicitEncrypted String value) {
} }
} }

106
src/main/asciidoc/reference/mongo-encryption.adoc

@ -2,7 +2,7 @@
= Client Side Field Level Encryption (CSFLE) = Client Side Field Level Encryption (CSFLE)
Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB. 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] [NOTE]
==== ====
@ -49,39 +49,42 @@ MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) {
[[mongo.encryption.explicit]] [[mongo.encryption.explicit]]
== Explicit Encryption == Explicit Encryption
Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform en-/decryption tasks. Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks.
The `@ExplicitlyEncrypted` annotation is a combination of the `@Encrypted` annotation used for <<mongo.jsonSchema.encrypted-fields,JSON Schema creation>> and a <<mongo.property-converters, Property Converter>>. The `@ExplicitEncrypted` annotation is a combination of the `@Encrypted` annotation used for <<mongo.jsonSchema.encrypted-fields,JSON Schema creation>> and a <<mongo.property-converters, Property Converter>>.
In other words, `@ExplicitlyEncrypted` uses existing building blocks and combines them to provide simplified support for explicit encryption. In other words, `@ExplicitEncrypted` uses existing building blocks to combine them for simplified explicit encryption support.
[NOTE] [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] [source,java]
---- ----
@ExplicitlyEncrypted(...) @ExplicitEncrypted(…)
String simpleValue; <1> String simpleValue; <1>
@ExplicitlyEncrypted(...) @ExplicitEncrypted(…)
Address address; <2> Address address; <2>
@ExplicitlyEncrypted(...) @ExplicitEncrypted(…)
List<...> list; <3> List<...> list; <3>
@ExplicitlyEncrypted(...) @ExplicitEncrypted(…)
Map<..., ...> mapOfString; <3> 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. <1> Encrypts the value of the simple type such as a `String` if not `null`.
<3> `Collection` like fields are encrypted entirely and not a value by value basis. <2> Encrypts the entire `Address` object and all its nested fields as `Document`.
<4> `Map` like fields are encrypted entirely and not on a key/value basis. 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. 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`. 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. 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. 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 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. 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] [source,java]
---- ----
@EncryptedField(algorithm = ..., altKeyName = "secret-key") <1> @EncryptedField(algorithm=…, altKeyName = "secret-key") <1>
String ssn; String ssn;
---- ----
[source,java] [source,java]
---- ----
@EncryptedField(algorithm = ..., altKeyName = "/name") <2> @EncryptedField(algorithm=…, altKeyName = "/name") <2>
String ssn; String ssn;
---- ----
<1> Use the DEK stored with the alternative name `secret-key`. <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. 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 <<mongo.property-converters>> section. To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the <<mongo.property-converters>> section.
[[mongo.encryption.explicit-setup]] [[mongo.encryption.explicit-setup]]
=== MongoEncryptionConverter Setup === MongoEncryptionConverter Setup
The default `MongoEncryptionConverter` needs to be registered within the `ApplicationContext`. The converter setup for `MongoEncryptionConverter` requires a few steps as several components are involved.
To do so we need to 1st setup the `Bean` and 2nd use a `BeanFactoryAwarePropertyValueConverterFactory` in the converter configuration. The bean setup consists of the following:
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`. 1. The `ClientEncryption` engine
The `EncryptionKeyResolver` provides the DEK to be used for encrypting the field. 2. A `MongoEncryptionConverter` instance configured with `ClientEncryption` and a `EncryptionKeyResolver`.
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. 3. A `PropertyValueConverterFactory` that uses the registered `MongoEncryptionConverter` bean.
`EncryptionKeyResolver.annotationBased(...)` offers an implementation that will lookup values from the `@ExplicitlyEncrypted` annotation before falling back to the context based resolution.
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 .Sample MongoEncryptionConverter Configuration
==== ====
@ -125,32 +133,38 @@ Since the `@ExplicitlyEncrypted` annotation does not need to specify an alt key
---- ----
class Config extends AbstractMongoClientConfiguration { class Config extends AbstractMongoClientConfiguration {
// ...
@Autowired ApplicationContext appContext; @Autowired ApplicationContext appContext;
@Bean @Bean
MongoEncryptionConverter encryptingConverter() { ClientEncryption clientEncryption() { <1>
ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder();
// …
ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder() return ClientEncryptions.create(encryptionSettings);
// ... }
@Bean
MongoEncryptionConverter encryptingConverter(ClientEncryption clientEncryption) {
Encryption<BsonValue, BsonBinary> encryption = MongoClientEncryption.just(ClientEncryptions.create(encryptionSettings)) <1> Encryption<BsonValue, BsonBinary> encryption = MongoClientEncryption.just(clientEncryption);
EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotationBased((ctx) -> ...); <2> EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotated((ctx) -> …); <2>
return new MongoEncryptionConverter(encryption, keyResolver); <3> return new MongoEncryptionConverter(encryption, keyResolver); <3>
} }
@Override @Override
protected void configureConverters(MongoConverterConfigurationAdapter adapter) { protected void configureConverters(MongoConverterConfigurationAdapter adapter) {
adapter adapter
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(appContext)); <4> .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`. <3> Create the `MongoEncryptionConverter`.
<4> Enable for a `PropertyValueConverter` within the `BeanFactory`. <4> Enable for a `PropertyValueConverter` lookup from the `BeanFactory`.
==== ====

Loading…
Cancel
Save