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
This commit is contained in:
@@ -17,16 +17,7 @@ package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.*;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@@ -39,7 +30,6 @@ import org.bson.codecs.configuration.CodecRegistry;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.json.JsonReader;
|
||||
import org.bson.types.ObjectId;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
@@ -51,16 +41,7 @@ import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.data.annotation.Reference;
|
||||
import org.springframework.data.convert.CustomConversions;
|
||||
import org.springframework.data.convert.TypeMapper;
|
||||
import org.springframework.data.mapping.AccessOptions;
|
||||
import org.springframework.data.mapping.Association;
|
||||
import org.springframework.data.mapping.InstanceCreatorMetadata;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.Parameter;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PersistentPropertyPath;
|
||||
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
|
||||
import org.springframework.data.mapping.*;
|
||||
import org.springframework.data.mapping.callback.EntityCallbacks;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
|
||||
@@ -902,7 +883,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
if (conversions.hasValueConverter(prop)) {
|
||||
accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj,
|
||||
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() {
|
||||
new MongoConversionContext(new PropertyValueProvider<>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public <T> T getPropertyValue(MongoPersistentProperty property) {
|
||||
@@ -1245,7 +1226,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
if (conversions.hasValueConverter(property)) {
|
||||
accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value,
|
||||
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() {
|
||||
new MongoConversionContext(new PropertyValueProvider<>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public <T> T getPropertyValue(MongoPersistentProperty property) {
|
||||
|
||||
@@ -15,17 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.convert.ValueConversionContext;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.PropertyValueProvider;
|
||||
import org.springframework.data.mapping.model.SpELContext;
|
||||
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -36,18 +31,20 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
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 MongoConverter mongoConverter;
|
||||
|
||||
@Nullable
|
||||
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);
|
||||
}
|
||||
|
||||
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.persistentProperty = persistentProperty;
|
||||
@@ -60,11 +57,13 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
|
||||
return persistentProperty;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Object getValue(String propertyPath) {
|
||||
return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
|
||||
return (T) mongoConverter.convertToMongoType(value, target);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.springframework.data.mongodb.core.convert.MongoValueConverter;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
|
||||
|
||||
/**
|
||||
* A specialized {@link MongoValueConverter} for {@literal en-/decrypting} properties.
|
||||
* A specialized {@link MongoValueConverter} for {@literal encryptiong} and {@literal decrypting} properties.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
@@ -32,20 +32,20 @@ public interface EncryptingConverter<S, T> extends MongoValueConverter<S, T> {
|
||||
return decrypt(value, buildEncryptionContext(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
default T write(Object value, MongoConversionContext context) {
|
||||
return encrypt(value, buildEncryptionContext(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the given encrypted source value within the given {@link EncryptionContext context}.
|
||||
*
|
||||
*
|
||||
* @param encryptedValue the encrypted source.
|
||||
* @param context the context to operate in.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
S decrypt(Object encryptedValue, EncryptionContext context);
|
||||
|
||||
@Override
|
||||
default T write(Object value, MongoConversionContext context) {
|
||||
return encrypt(value, buildEncryptionContext(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt the given raw source value within the given {@link EncryptionContext context}.
|
||||
*
|
||||
|
||||
@@ -39,6 +39,9 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Default implementation of {@link EncryptingConverter}. Properties used with this converter must be annotated with
|
||||
* {@link Encrypted @Encrypted} to provide key and algorithm metadata.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
@@ -46,7 +49,7 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class);
|
||||
|
||||
private Encryption<BsonValue, BsonBinary> encryption;
|
||||
private final Encryption<BsonValue, BsonBinary> encryption;
|
||||
private final 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 (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("Decrypting %s.%s.", getProperty(context).getOwner().getName(),
|
||||
LOGGER.debug(String.format("Decrypting %s.%s.", getProperty(context).getOwner().getName(),
|
||||
getProperty(context).getName()));
|
||||
}
|
||||
|
||||
@@ -83,18 +86,20 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
|
||||
}
|
||||
}
|
||||
|
||||
MongoPersistentProperty persistentProperty = getProperty(context);
|
||||
MongoPersistentProperty persistentProperty = getProperty(context);
|
||||
|
||||
if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable<?> iterable) {
|
||||
|
||||
int size = iterable instanceof Collection<?> c ? c.size() : 10;
|
||||
|
||||
if (!persistentProperty.isEntity()) {
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), 10);
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
|
||||
iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it)));
|
||||
return collection;
|
||||
} else {
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), 10);
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), size);
|
||||
iterable.forEach(it -> {
|
||||
collection.add(context.read(BsonUtils.toJavaType((BsonValue) it),
|
||||
persistentProperty.getActualType()));
|
||||
collection.add(context.read(BsonUtils.toJavaType((BsonValue) it), persistentProperty.getActualType()));
|
||||
});
|
||||
return collection;
|
||||
}
|
||||
@@ -109,17 +114,12 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
|
||||
}
|
||||
|
||||
if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) {
|
||||
return context.read(BsonUtils.toJavaType(bsonDocument),
|
||||
persistentProperty.getTypeInformation().getType());
|
||||
return context.read(BsonUtils.toJavaType(bsonDocument), persistentProperty.getTypeInformation().getType());
|
||||
}
|
||||
|
||||
return decryptedValue;
|
||||
}
|
||||
|
||||
private MongoPersistentProperty getProperty(EncryptionContext context) {
|
||||
return context.getProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object encrypt(Object value, EncryptionContext context) {
|
||||
|
||||
@@ -128,15 +128,19 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
|
||||
getProperty(context).getName()));
|
||||
}
|
||||
|
||||
MongoPersistentProperty persistentProperty = getProperty(context);
|
||||
MongoPersistentProperty persistentProperty = getProperty(context);
|
||||
|
||||
Encrypted annotation = persistentProperty.findAnnotation(Encrypted.class);
|
||||
if(annotation == null) {
|
||||
if (annotation == null) {
|
||||
annotation = persistentProperty.getOwner().findAnnotation(Encrypted.class);
|
||||
}
|
||||
|
||||
EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm());
|
||||
encryptionOptions.setKey(keyResolver.getKey(context));
|
||||
if (annotation == null) {
|
||||
throw new IllegalStateException(String.format("Property %s.%s is not annotated with @Encrypted",
|
||||
getProperty(context).getOwner().getName(), getProperty(context).getName()));
|
||||
}
|
||||
|
||||
EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm(), keyResolver.getKey(context));
|
||||
|
||||
if (!persistentProperty.isEntity()) {
|
||||
|
||||
@@ -162,36 +166,44 @@ public class MongoEncryptionConverter implements EncryptingConverter<Object, Obj
|
||||
return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions);
|
||||
}
|
||||
|
||||
public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,
|
||||
private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,
|
||||
EncryptionContext context) {
|
||||
|
||||
BsonArray bsonArray = new BsonArray();
|
||||
if (!property.isEntity()) {
|
||||
if (value instanceof Collection values) {
|
||||
values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it)));
|
||||
} else if (ObjectUtils.isArray(value)) {
|
||||
for (Object o : ObjectUtils.toObjectArray(value)) {
|
||||
boolean isEntity = property.isEntity();
|
||||
|
||||
if (value instanceof Collection<?> values) {
|
||||
values.forEach(it -> {
|
||||
|
||||
if (isEntity) {
|
||||
Document document = (Document) context.write(it, property.getTypeInformation());
|
||||
bsonArray.add(document == null ? null : document.toBsonDocument());
|
||||
} else {
|
||||
bsonArray.add(BsonUtils.simpleToBsonValue(it));
|
||||
}
|
||||
});
|
||||
} else if (ObjectUtils.isArray(value)) {
|
||||
|
||||
for (Object o : ObjectUtils.toObjectArray(value)) {
|
||||
|
||||
if (isEntity) {
|
||||
Document document = (Document) context.write(o, property.getTypeInformation());
|
||||
bsonArray.add(document == null ? null : document.toBsonDocument());
|
||||
} else {
|
||||
bsonArray.add(BsonUtils.simpleToBsonValue(o));
|
||||
}
|
||||
}
|
||||
return bsonArray;
|
||||
} else {
|
||||
if (value instanceof Collection values) {
|
||||
values.forEach(it -> {
|
||||
Document write = (Document) context.write(it, property.getTypeInformation());
|
||||
bsonArray.add(write.toBsonDocument());
|
||||
});
|
||||
} else if (ObjectUtils.isArray(value)) {
|
||||
for (Object o : ObjectUtils.toObjectArray(value)) {
|
||||
Document write = (Document) context.write(o, property.getTypeInformation());
|
||||
bsonArray.add(write.toBsonDocument());
|
||||
}
|
||||
}
|
||||
return bsonArray;
|
||||
}
|
||||
|
||||
return bsonArray;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EncryptionContext buildEncryptionContext(MongoConversionContext context) {
|
||||
return new ExplicitEncryptionContext(context);
|
||||
}
|
||||
|
||||
protected MongoPersistentProperty getProperty(EncryptionContext context) {
|
||||
return context.getProperty();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -16,7 +16,7 @@
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
/**
|
||||
* Component responsible for en-/decrypting values.
|
||||
* Component responsible for encrypting and decrypting values.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import org.springframework.data.convert.ValueConversionContext;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
@@ -23,7 +22,10 @@ import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Context to encapsulate encryption for a specific {@link MongoPersistentProperty}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface EncryptionContext {
|
||||
|
||||
@@ -36,7 +38,7 @@ public interface EncryptionContext {
|
||||
|
||||
/**
|
||||
* Shortcut for converting a given {@literal value} into its store representation using the root
|
||||
* {@link ValueConversionContext}.
|
||||
* {@code ValueConversionContext}.
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
|
||||
@@ -16,20 +16,45 @@
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.BsonBinarySubType;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* The {@link EncryptionKey} represents a {@literal Data Encryption Key} reference that can be either direct via the
|
||||
* {@link KeyId key id} or its {@link AltKeyName Key Alternative Name}.
|
||||
*
|
||||
* {@link KeyId key id} or its {@link KeyAltName Key Alternative Name}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface EncryptionKey {
|
||||
|
||||
/**
|
||||
* @return the value that allows to reference a specific key
|
||||
* Create a new {@link EncryptionKey} that uses the keys id for reference.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @return new instance of {@link EncryptionKey KeyId}.
|
||||
*/
|
||||
static EncryptionKey keyId(BsonBinary key) {
|
||||
|
||||
Assert.notNull(key, "KeyId must not be null");
|
||||
|
||||
return new KeyId(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link EncryptionKey} that uses an {@literal Key Alternative Name} for reference.
|
||||
*
|
||||
* @param keyAltName must not be {@literal null} or empty.
|
||||
* @return new instance of {@link EncryptionKey KeyAltName}.
|
||||
*/
|
||||
static EncryptionKey keyAltName(String keyAltName) {
|
||||
|
||||
Assert.hasText(keyAltName, "Key Alternative Name must not be empty");
|
||||
|
||||
return new KeyAltName(keyAltName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the value that allows to reference a specific key.
|
||||
*/
|
||||
Object value();
|
||||
|
||||
@@ -38,107 +63,6 @@ public interface EncryptionKey {
|
||||
*/
|
||||
Type type();
|
||||
|
||||
/**
|
||||
* Create a new {@link EncryptionKey} that uses the keys id for reference.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @return new instance of {@link KeyId}.
|
||||
*/
|
||||
static KeyId keyId(BsonBinary key) {
|
||||
return new KeyId(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link EncryptionKey} that uses an {@literal Key Alternative Name} for reference.
|
||||
*
|
||||
* @param altKeyName must not be {@literal null}.
|
||||
* @return new instance of {@link KeyId}.
|
||||
*/
|
||||
static AltKeyName altKeyName(String altKeyName) {
|
||||
return new AltKeyName(altKeyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value must not be {@literal null}.
|
||||
*/
|
||||
record KeyId(BsonBinary value) implements EncryptionKey {
|
||||
|
||||
@Override
|
||||
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;
|
||||
}
|
||||
|
||||
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}.
|
||||
*/
|
||||
record AltKeyName(String value) implements EncryptionKey {
|
||||
|
||||
@Override
|
||||
public Type type() {
|
||||
return Type.ALT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
if (value().length() <= 3) {
|
||||
return "AltKeyName('***')";
|
||||
}
|
||||
return String.format("AltKeyName('%s***')", value.substring(0, 3));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AltKeyName that = (AltKeyName) o;
|
||||
return ObjectUtils.nullSafeEquals(value, that.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return ObjectUtils.nullSafeHashCode(value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The key reference type.
|
||||
*/
|
||||
|
||||
@@ -18,18 +18,20 @@ package org.springframework.data.mongodb.core.encryption;
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.types.Binary;
|
||||
import org.springframework.data.mongodb.core.mapping.Encrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.util.BsonUtils;
|
||||
import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Interface to obtain a {@link EncryptionKey Data Encryption Key} that is valid in a given {@link EncryptionContext
|
||||
* context}.
|
||||
* <p>
|
||||
* Use the {@link #annotationBased(EncryptionKeyResolver) based} variant which will first try to resolve a potential
|
||||
* {@link ExplicitlyEncrypted#altKeyName() Key Alternate Name} from annotations before calling the fallback resolver.
|
||||
*
|
||||
* Use the {@link #annotated(EncryptionKeyResolver) based} variant which will first try to resolve a potential
|
||||
* {@link ExplicitEncrypted#keyAltName() Key Alternate Name} from annotations before calling the fallback resolver.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
* @see EncryptionKey
|
||||
@@ -46,20 +48,23 @@ public interface EncryptionKeyResolver {
|
||||
EncryptionKey getKey(EncryptionContext encryptionContext);
|
||||
|
||||
/**
|
||||
* Obtain an {@link EncryptionKeyResolver} that evaluates {@link ExplicitlyEncrypted#altKeyName()} and only calls the
|
||||
* Obtain an {@link EncryptionKeyResolver} that evaluates {@link ExplicitEncrypted#keyAltName()} and only calls the
|
||||
* fallback {@link EncryptionKeyResolver resolver} if no {@literal Key Alternate Name} is present.
|
||||
*
|
||||
*
|
||||
* @param fallback must not be {@literal null}.
|
||||
* @return new instance of {@link EncryptionKeyResolver}.
|
||||
*/
|
||||
static EncryptionKeyResolver annotationBased(EncryptionKeyResolver fallback) {
|
||||
static EncryptionKeyResolver annotated(EncryptionKeyResolver fallback) {
|
||||
|
||||
Assert.notNull(fallback, "Fallback EncryptionKeyResolver must not be nul");
|
||||
|
||||
return ((encryptionContext) -> {
|
||||
|
||||
ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class);
|
||||
if (annotation == null || !StringUtils.hasText(annotation.altKeyName())) {
|
||||
MongoPersistentProperty property = encryptionContext.getProperty();
|
||||
ExplicitEncrypted annotation = property.findAnnotation(ExplicitEncrypted.class);
|
||||
if (annotation == null || !StringUtils.hasText(annotation.keyAltName())) {
|
||||
|
||||
Encrypted encrypted = encryptionContext.getProperty().getOwner().findAnnotation(Encrypted.class);
|
||||
Encrypted encrypted = property.getOwner().findAnnotation(Encrypted.class);
|
||||
if (encrypted == null) {
|
||||
return fallback.getKey(encryptionContext);
|
||||
}
|
||||
@@ -73,19 +78,22 @@ public interface EncryptionKeyResolver {
|
||||
return EncryptionKey.keyId((BsonBinary) BsonUtils.simpleToBsonValue(binary));
|
||||
}
|
||||
if (o instanceof String string) {
|
||||
return EncryptionKey.altKeyName(string);
|
||||
return EncryptionKey.keyAltName(string);
|
||||
}
|
||||
|
||||
throw new IllegalStateException(String.format("Cannot determine encryption key for %s.%s using key type %s",
|
||||
property.getOwner().getName(), property.getName(), o == null ? "null" : o.getClass().getName()));
|
||||
}
|
||||
|
||||
String altKeyName = annotation.altKeyName();
|
||||
if (altKeyName.startsWith("/")) {
|
||||
Object fieldValue = encryptionContext.lookupValue(altKeyName.replace("/", ""));
|
||||
String keyAltName = annotation.keyAltName();
|
||||
if (keyAltName.startsWith("/")) {
|
||||
Object fieldValue = encryptionContext.lookupValue(keyAltName.replace("/", ""));
|
||||
if (fieldValue == null) {
|
||||
throw new IllegalStateException(String.format("Key Alternative Name for %s was null", altKeyName));
|
||||
throw new IllegalStateException(String.format("Key Alternative Name for %s was null", keyAltName));
|
||||
}
|
||||
return new EncryptionKey.AltKeyName(fieldValue.toString());
|
||||
return new KeyAltName(fieldValue.toString());
|
||||
} else {
|
||||
return new EncryptionKey.AltKeyName(altKeyName);
|
||||
return new KeyAltName(keyAltName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
@@ -27,16 +27,15 @@ import org.springframework.util.ObjectUtils;
|
||||
public class EncryptionOptions {
|
||||
|
||||
private final String algorithm;
|
||||
private @Nullable EncryptionKey key;
|
||||
private final EncryptionKey key;
|
||||
|
||||
public EncryptionOptions(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
public EncryptionOptions(String algorithm, EncryptionKey key) {
|
||||
|
||||
public EncryptionOptions setKey(EncryptionKey key) {
|
||||
Assert.hasText(algorithm, "Algorithm must not be empty");
|
||||
Assert.notNull(key, "EncryptionKey must not be empty");
|
||||
|
||||
this.key = key;
|
||||
return this;
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public EncryptionKey key() {
|
||||
@@ -47,11 +46,6 @@ public class EncryptionOptions {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
||||
@@ -77,4 +71,9 @@ public class EncryptionOptions {
|
||||
result = 31 * result + ObjectUtils.nullSafeHashCode(key);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.bson.BsonBinary;
|
||||
@@ -35,23 +34,9 @@ import com.mongodb.client.vault.ClientEncryption;
|
||||
public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> {
|
||||
|
||||
private final Supplier<ClientEncryption> source;
|
||||
private final AtomicReference<ClientEncryption> cached;
|
||||
|
||||
private MongoClientEncryption(Supplier<ClientEncryption> source) {
|
||||
|
||||
MongoClientEncryption(Supplier<ClientEncryption> 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");
|
||||
|
||||
return new MongoClientEncryption(() -> clientEncryption) {
|
||||
@Override
|
||||
public boolean refresh() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if refreshed, {@literal false} otherwise.
|
||||
*/
|
||||
public boolean refresh() {
|
||||
cached.set(source.get());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ClientEncryption#close() Shutdown} the underlying {@link ClientEncryption}.
|
||||
*/
|
||||
public void shutdown() {
|
||||
getClientEncryption().close();
|
||||
return new MongoClientEncryption(() -> clientEncryption);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,7 +72,7 @@ public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary>
|
||||
}
|
||||
|
||||
public ClientEncryption getClientEncryption() {
|
||||
return cached.get();
|
||||
return source.get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -27,13 +27,13 @@ import org.springframework.data.mongodb.core.convert.encryption.EncryptingConver
|
||||
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
|
||||
|
||||
/**
|
||||
* {@link ExplicitlyEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that
|
||||
* indicates the target element is subject to encryption during the mapping process, in which a given domain type is
|
||||
* converted into the store specific format.
|
||||
* {@link ExplicitEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that indicates
|
||||
* the target element is subject to encryption during the mapping process, in which a given domain type is converted
|
||||
* into the store specific format.
|
||||
* <p>
|
||||
* The {@link #value()} attribute, defines the bean type to look up within the
|
||||
* {@link org.springframework.context.ApplicationContext} to obtain the {@link EncryptingConverter} responsible for the
|
||||
* actual {@literal en-/decryption} while {@link #algorithm()} and {@link #altKeyName()} can be used to define aspects
|
||||
* actual {@literal en-/decryption} while {@link #algorithm()} and {@link #keyAltName()} can be used to define aspects
|
||||
* of the encryption process.
|
||||
*
|
||||
* <pre class="code">
|
||||
@@ -41,11 +41,11 @@ import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionC
|
||||
* private ObjectId id;
|
||||
* private String name;
|
||||
*
|
||||
* @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "secred-key-alternative-name") //
|
||||
* @ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "secred-key-alternative-name") //
|
||||
* private String ssn;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
* @see ValueConverter
|
||||
@@ -54,7 +54,7 @@ import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionC
|
||||
@Target(ElementType.FIELD)
|
||||
@Encrypted
|
||||
@ValueConverter
|
||||
public @interface ExplicitlyEncrypted {
|
||||
public @interface ExplicitEncrypted {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return the string representation of the encryption algorithm to use.
|
||||
* @see org.springframework.data.mongodb.core.EncryptionAlgorithms
|
||||
*/
|
||||
@AliasFor(annotation = Encrypted.class, value = "algorithm")
|
||||
String algorithm() default "";
|
||||
@@ -78,10 +79,10 @@ public @interface ExplicitlyEncrypted {
|
||||
* It is possible to use the {@literal "/"} character as a prefix to access a particular field value in the same
|
||||
* domain type. In this case {@code "/name"} references the value of the {@literal name} field. Please note that
|
||||
* update operations will require the full object to resolve those values.
|
||||
*
|
||||
*
|
||||
* @return the {@literal Key Alternate Name} if set or an empty {@link String}.
|
||||
*/
|
||||
String altKeyName() default "";
|
||||
String keyAltName() default "";
|
||||
|
||||
/**
|
||||
* The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property.
|
||||
@@ -35,11 +35,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.mongodb.core.mapping.Encrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link EncryptionKeyResolver}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -65,7 +67,7 @@ class EncryptionKeyResolverUnitTests {
|
||||
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
|
||||
AnnotatedWithExplicitlyEncrypted::getNotAnnotated);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isSameAs(defaultEncryptionKey);
|
||||
}
|
||||
@@ -76,7 +78,7 @@ class EncryptionKeyResolverUnitTests {
|
||||
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
|
||||
AnnotatedWithExplicitlyEncrypted::getAlgorithm);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isSameAs(defaultEncryptionKey);
|
||||
}
|
||||
@@ -87,9 +89,9 @@ class EncryptionKeyResolverUnitTests {
|
||||
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
|
||||
AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyName);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.altKeyName("sec-key-name"));
|
||||
assertThat(key).isEqualTo(EncryptionKey.keyAltName("sec-key-name"));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
@@ -99,9 +101,9 @@ class EncryptionKeyResolverUnitTests {
|
||||
AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyNameFromPropertyValue);
|
||||
when(ctx.lookupValue(eq("notAnnotated"))).thenReturn("born-to-be-wild");
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.altKeyName("born-to-be-wild"));
|
||||
assertThat(key).isEqualTo(EncryptionKey.keyAltName("born-to-be-wild"));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
@@ -111,7 +113,7 @@ class EncryptionKeyResolverUnitTests {
|
||||
AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType.class,
|
||||
AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType::getKeyIdFromDomainType);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.keyId(
|
||||
new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g=="))));
|
||||
@@ -127,7 +129,7 @@ class EncryptionKeyResolverUnitTests {
|
||||
|
||||
when(ctx.getEvaluationContext(any())).thenReturn(evaluationContext);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
EncryptionKey key = EncryptionKeyResolver.annotated(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.keyId(
|
||||
new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g=="))));
|
||||
@@ -145,13 +147,13 @@ class EncryptionKeyResolverUnitTests {
|
||||
|
||||
String notAnnotated;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
String algorithm;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "sec-key-name") //
|
||||
String algorithmAndAltKeyName;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/notAnnotated") //
|
||||
String algorithmAndAltKeyNameFromPropertyValue;
|
||||
}
|
||||
|
||||
@@ -159,10 +161,10 @@ class EncryptionKeyResolverUnitTests {
|
||||
@Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==")
|
||||
class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType {
|
||||
|
||||
@ExplicitlyEncrypted //
|
||||
@ExplicitEncrypted //
|
||||
String keyIdFromDomainType;
|
||||
|
||||
@ExplicitlyEncrypted(altKeyName = "sec-key-name") //
|
||||
@ExplicitEncrypted(keyAltName = "sec-key-name") //
|
||||
String altKeyNameFromPropertyIgnoringKeyIdFromDomainType;
|
||||
}
|
||||
|
||||
@@ -170,7 +172,7 @@ class EncryptionKeyResolverUnitTests {
|
||||
@Encrypted(keyId = "#{#myKeyId}")
|
||||
class KeyIdFromSpel {
|
||||
|
||||
@ExplicitlyEncrypted //
|
||||
@ExplicitEncrypted //
|
||||
String keyIdFromDomainType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import org.bson.UuidRepresentation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link EncryptionKey}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class EncryptionKeyUnitTests {
|
||||
@@ -40,9 +42,9 @@ class EncryptionKeyUnitTests {
|
||||
@Test // GH-4284
|
||||
void altKeyNameToStringDoesNotRevealEntireKey() {
|
||||
|
||||
assertThat(EncryptionKey.altKeyName("s").toString()).contains("***");
|
||||
assertThat(EncryptionKey.altKeyName("su").toString()).contains("***");
|
||||
assertThat(EncryptionKey.altKeyName("sup").toString()).contains("***");
|
||||
assertThat(EncryptionKey.altKeyName("super-secret-key").toString()).contains("sup***");
|
||||
assertThat(EncryptionKey.keyAltName("s").toString()).contains("***");
|
||||
assertThat(EncryptionKey.keyAltName("su").toString()).contains("***");
|
||||
assertThat(EncryptionKey.keyAltName("sup").toString()).contains("***");
|
||||
assertThat(EncryptionKey.keyAltName("super-secret-key").toString()).contains("sup***");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@ import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.bson.BsonBinary;
|
||||
@@ -39,6 +41,7 @@ import org.bson.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;
|
||||
@@ -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.encryption.MongoEncryptionConverter;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
@@ -68,6 +71,7 @@ import com.mongodb.client.model.Filters;
|
||||
import com.mongodb.client.model.IndexOptions;
|
||||
import com.mongodb.client.model.Indexes;
|
||||
import com.mongodb.client.model.vault.DataKeyOptions;
|
||||
import com.mongodb.client.vault.ClientEncryption;
|
||||
import com.mongodb.client.vault.ClientEncryptions;
|
||||
|
||||
/**
|
||||
@@ -80,7 +84,7 @@ public class EncryptionTests {
|
||||
@Autowired MongoTemplate template;
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptSimpleValue() {
|
||||
void encryptAndDecryptSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
@@ -95,7 +99,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptComplexValue() {
|
||||
void encryptAndDecryptComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
@@ -112,7 +116,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptValueWithinComplexOne() {
|
||||
void encryptAndDecryptValueWithinComplexOne() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
@@ -135,7 +139,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptListOfSimpleValue() {
|
||||
void encryptAndDecryptListOfSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
@@ -150,7 +154,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptListOfComplexValue() {
|
||||
void encryptAndDecryptListOfComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
@@ -170,7 +174,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptMapOfSimpleValues() {
|
||||
void encryptAndDecryptMapOfSimpleValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
@@ -185,7 +189,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptMapOfComplexValues() {
|
||||
void encryptAndDecryptMapOfComplexValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
@@ -312,7 +316,7 @@ public class EncryptionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) throws InterruptedException {
|
||||
void altKeyDetection(@Autowired CachingMongoClientEncryption mongoClientEncryption) throws InterruptedException {
|
||||
|
||||
BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("user-1")));
|
||||
@@ -349,7 +353,7 @@ public class EncryptionTests {
|
||||
mongoClientEncryption.getClientEncryption().deleteKey(user2key);
|
||||
|
||||
// clear the 60 second key cache within the mongo client
|
||||
mongoClientEncryption.refresh();
|
||||
mongoClientEncryption.destroy();
|
||||
|
||||
assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1);
|
||||
|
||||
@@ -444,12 +448,12 @@ public class EncryptionTests {
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))));
|
||||
|
||||
return new MongoEncryptionConverter(mongoClientEncryption,
|
||||
EncryptionKeyResolver.annotationBased((ctx) -> EncryptionKey.keyId(dataKey.get())));
|
||||
EncryptionKeyResolver.annotated((ctx) -> EncryptionKey.keyId(dataKey.get())));
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
|
||||
return MongoClientEncryption.caching(() -> ClientEncryptions.create(encryptionSettings));
|
||||
CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
|
||||
return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -468,9 +472,9 @@ public class EncryptionTests {
|
||||
|
||||
final byte[] localMasterKey = new byte[96];
|
||||
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);
|
||||
}
|
||||
@@ -485,6 +489,36 @@ public class EncryptionTests {
|
||||
.keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build();
|
||||
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
|
||||
@@ -494,30 +528,30 @@ public class EncryptionTests {
|
||||
String id;
|
||||
String name;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
String ssn;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "mySuperSecretKey") //
|
||||
String wallet;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random
|
||||
Address address;
|
||||
|
||||
AddressWithEncryptedZip encryptedZip;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
|
||||
List<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;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/name") //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/name") //
|
||||
String viaAltKeyNameField;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
Map<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;
|
||||
}
|
||||
|
||||
@@ -531,7 +565,7 @@ public class EncryptionTests {
|
||||
@Setter
|
||||
static class AddressWithEncryptedZip extends Address {
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip;
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
|
||||
@@ -33,6 +33,8 @@ import com.mongodb.client.model.vault.EncryptOptions;
|
||||
import com.mongodb.client.vault.ClientEncryption;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MongoClientEncryption}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -55,7 +57,7 @@ class MongoClientEncryptionUnitTests {
|
||||
|
||||
MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption);
|
||||
mce.encrypt(new BsonBinary(new byte[0]),
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name")));
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random, EncryptionKey.keyAltName("sec-key-name")));
|
||||
|
||||
ArgumentCaptor<EncryptOptions> options = ArgumentCaptor.forClass(EncryptOptions.class);
|
||||
verify(clientEncryption).encrypt(any(), options.capture());
|
||||
@@ -63,23 +65,12 @@ class MongoClientEncryptionUnitTests {
|
||||
assertThat(options.getValue().getKeyAltName()).isEqualTo("sec-key-name");
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void refreshHasNoEffectForFixedClientEncryption() {
|
||||
|
||||
MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption);
|
||||
mce.decrypt(new BsonBinary(new byte[0]));
|
||||
|
||||
assertThat(mce.getClientEncryption()).isSameAs(clientEncryption);
|
||||
assertThat(mce.refresh()).isFalse();
|
||||
assertThat(mce.getClientEncryption()).isSameAs(clientEncryption);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void refreshObtainsNextInstanceFromSupplier() {
|
||||
|
||||
ClientEncryption next = mock(ClientEncryption.class);
|
||||
|
||||
MongoClientEncryption mce = MongoClientEncryption.caching(new Supplier<>() {
|
||||
MongoClientEncryption mce = new MongoClientEncryption(new Supplier<>() {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
@@ -90,7 +81,6 @@ class MongoClientEncryptionUnitTests {
|
||||
});
|
||||
|
||||
assertThat(mce.getClientEncryption()).isSameAs(clientEncryption);
|
||||
assertThat(mce.refresh()).isTrue();
|
||||
assertThat(mce.getClientEncryption()).isSameAs(next);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
|
||||
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
|
||||
|
||||
@@ -72,10 +72,10 @@ class MongoEncryptionConverterUnitTests {
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
|
||||
when(fallbackKeyResolver.getKey(any())).thenReturn(EncryptionKey.altKeyName("default"));
|
||||
when(fallbackKeyResolver.getKey(any())).thenReturn(EncryptionKey.keyAltName("default"));
|
||||
when(encryption.encrypt(valueToBeEncrypted.capture(), encryptionOptions.capture()))
|
||||
.thenReturn(new BsonBinary(new byte[0]));
|
||||
keyResolver = EncryptionKeyResolver.annotationBased(fallbackKeyResolver);
|
||||
keyResolver = EncryptionKeyResolver.annotated(fallbackKeyResolver);
|
||||
converter = new MongoEncryptionConverter(encryption, keyResolver);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class MongoEncryptionConverterUnitTests {
|
||||
|
||||
assertThat(valueToBeEncrypted.getValue()).isEqualTo(new BsonString("foo"));
|
||||
assertThat(encryptionOptions.getValue()).isEqualTo(
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic).setKey(EncryptionKey.altKeyName("default")));
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic, EncryptionKey.keyAltName("default")));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
@@ -101,7 +101,7 @@ class MongoEncryptionConverterUnitTests {
|
||||
converter.write("foo", conversionContext);
|
||||
|
||||
assertThat(encryptionOptions.getValue()).isEqualTo(
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name")));
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random, EncryptionKey.keyAltName("sec-key-name")));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
@@ -117,7 +117,7 @@ class MongoEncryptionConverterUnitTests {
|
||||
assertThat(path.getValue()).isEqualTo("notAnnotated");
|
||||
|
||||
assertThat(encryptionOptions.getValue())
|
||||
.isEqualTo(new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("(ツ)")));
|
||||
.isEqualTo(new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random, EncryptionKey.keyAltName("(ツ)")));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
@@ -216,33 +216,33 @@ class MongoEncryptionConverterUnitTests {
|
||||
|
||||
String notAnnotated;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
String stringValueWithAlgorithmOnly;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "sec-key-name") //
|
||||
String stringValueWithAlgorithmAndAltKeyName;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, keyAltName = "/notAnnotated") //
|
||||
String stringValueWithAlgorithmAndAltKeyNameFromPropertyValue;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random
|
||||
JustATypeWithAnUnencryptedField nestedFullyEncrypted;
|
||||
|
||||
NestedWithEncryptedField nestedWithEncryptedField;
|
||||
|
||||
// Client-Side Field Level Encryption does not support encrypting individual array elements
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
List<String> listOfString;
|
||||
|
||||
// Client-Side Field Level Encryption does not support encrypting individual array elements
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
|
||||
List<JustATypeWithAnUnencryptedField> listOfComplex;
|
||||
|
||||
// just as it was a domain type encrypt the entire thing here
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
Map<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;
|
||||
|
||||
RecordWithEncryptedValue recordWithEncryptedValue;
|
||||
@@ -257,10 +257,10 @@ class MongoEncryptionConverterUnitTests {
|
||||
|
||||
static class NestedWithEncryptedField extends JustATypeWithAnUnencryptedField {
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
String encryptedValue;
|
||||
}
|
||||
|
||||
record RecordWithEncryptedValue(@ExplicitlyEncrypted String value) {
|
||||
record RecordWithEncryptedValue(@ExplicitEncrypted String value) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
= Client Side Field Level Encryption (CSFLE)
|
||||
|
||||
Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB.
|
||||
Please make sure to read the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions.
|
||||
We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
@@ -49,39 +49,42 @@ MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) {
|
||||
[[mongo.encryption.explicit]]
|
||||
== Explicit Encryption
|
||||
|
||||
Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform en-/decryption tasks.
|
||||
The `@ExplicitlyEncrypted` annotation is a combination of the `@Encrypted` annotation used for <<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.
|
||||
Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks.
|
||||
The `@ExplicitEncrypted` annotation is a combination of the `@Encrypted` annotation used for <<mongo.jsonSchema.encrypted-fields,JSON Schema creation>> and a <<mongo.property-converters, Property Converter>>.
|
||||
In other words, `@ExplicitEncrypted` uses existing building blocks to combine them for simplified explicit encryption support.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Fields annotated with `@ExplicitlyEncrypted` are always encrypted entirely as outlined in below.
|
||||
Fields annotated with `@ExplicitEncrypted` are always encrypted as whole.
|
||||
Consider the following example:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@ExplicitlyEncrypted(...)
|
||||
String simpleValue; <1>
|
||||
@ExplicitEncrypted(…)
|
||||
String simpleValue; <1>
|
||||
|
||||
@ExplicitlyEncrypted(...)
|
||||
Address address; <2>
|
||||
@ExplicitEncrypted(…)
|
||||
Address address; <2>
|
||||
|
||||
@ExplicitlyEncrypted(...)
|
||||
List<...> list; <3>
|
||||
@ExplicitEncrypted(…)
|
||||
List<...> list; <3>
|
||||
|
||||
@ExplicitlyEncrypted(...)
|
||||
Map<..., ...> mapOfString; <3>
|
||||
@ExplicitEncrypted(…)
|
||||
Map<..., ...> mapOfString; <4>
|
||||
----
|
||||
<1> Encrypts the value of the simple type eg. a `String` if not `null`.
|
||||
<2> Encrypts the entire `Address` object and all its nested fields. To only encrypt parts of the `Address`, like `Address#street` the `street` field needs to be annotated.
|
||||
<3> `Collection` like fields are encrypted entirely and not a value by value basis.
|
||||
<4> `Map` like fields are encrypted entirely and not on a key/value basis.
|
||||
|
||||
<1> Encrypts the value of the simple type such as a `String` if not `null`.
|
||||
<2> Encrypts the entire `Address` object and all its nested fields as `Document`.
|
||||
To only encrypt parts of the `Address`, like `Address#street` the `street` field within `Address` needs to be annotated with `@ExplicitEncrypted`.
|
||||
<3> ``Collection``-like fields are encrypted as single value and not per entry.
|
||||
<4> ``Map``-like fields are encrypted as single value and not as a key/value entry.
|
||||
====
|
||||
|
||||
Depending on the encryption algorithm MongoDB supports certain operations on an encrypted field using its https://www.mongodb.com/docs/manual/core/queryable-encryption/[Queryable Encryption] feature.
|
||||
To pick a certain algorithm use `@ExplicitlyEncrypted(algorithm = ... )` and choose the required one via `EncryptionAlgorithms`.
|
||||
Depending on the encryption algorithm, MongoDB supports certain operations on an encrypted field using its https://www.mongodb.com/docs/manual/core/queryable-encryption/[Queryable Encryption] feature.
|
||||
To pick a certain algorithm use `@ExplicitEncrypted(algorithm)`, see `EncryptionAlgorithms` for algorithm constants.
|
||||
Please read the https://www.mongodb.com/docs/manual/core/csfle/fundamentals/encryption-algorithms[Encryption Types] manual for more information on algorithms and their usage.
|
||||
|
||||
To perform the actual encryption we do also need a Data Encryption Key (DEK).
|
||||
To perform the actual encryption we require a Data Encryption Key (DEK).
|
||||
Please refer to the https://www.mongodb.com/docs/manual/core/csfle/quick-start/#create-a-data-encryption-key[MongoDB Documentation] for more information on how to set up key management and create a Data Encryption Key.
|
||||
The DEK can be referenced directly via its `id` or a defined _alternative name_.
|
||||
The `@EncryptedField` annotation only allows referencing a DEK via an alternative name.
|
||||
@@ -91,33 +94,38 @@ It is possible to provide an `EncryptionKeyResolver`, which will be discussed la
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@EncryptedField(algorithm = ..., altKeyName = "secret-key") <1>
|
||||
@EncryptedField(algorithm=…, altKeyName = "secret-key") <1>
|
||||
String ssn;
|
||||
----
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@EncryptedField(algorithm = ..., altKeyName = "/name") <2>
|
||||
@EncryptedField(algorithm=…, altKeyName = "/name") <2>
|
||||
String ssn;
|
||||
----
|
||||
|
||||
<1> Use the DEK stored with the alternative name `secret-key`.
|
||||
<2> Uses a field reference that will read the actual field value and use that for key lookup. Always requires the full document to be present for save operations. Fields cannot be used in queries/aggregations.
|
||||
<2> Uses a field reference that will read the actual field value and use that for key lookup.
|
||||
Always requires the full document to be present for save operations.
|
||||
Fields cannot be used in queries/aggregations.
|
||||
====
|
||||
|
||||
By default the `@ExplicitlyEncrypted(value=...)` attribute will reference a `MongoEncryptionConverter`.
|
||||
By default, the `@ExplicitEncrypted(value=…)` attribute references a `MongoEncryptionConverter`.
|
||||
It is possible to change the default implementation and exchange it with any `PropertyValueConverter` implementation by providing the according type reference.
|
||||
To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the <<mongo.property-converters>> section.
|
||||
|
||||
[[mongo.encryption.explicit-setup]]
|
||||
=== MongoEncryptionConverter Setup
|
||||
|
||||
The default `MongoEncryptionConverter` needs to be registered within the `ApplicationContext`.
|
||||
To do so we need to 1st setup the `Bean` and 2nd use a `BeanFactoryAwarePropertyValueConverterFactory` in the converter configuration.
|
||||
The converter itself needs to know about the actual `Encryption` that is capable of en-/decrypting `BsonValue` to/from `BsonBinary` as well as a `EncryptionKeyResolver`.
|
||||
`MongoClientEncryption` is the default implementation delegating en-/decryption to `com.mongodb.client.vault.ClientEncryption`.
|
||||
The `EncryptionKeyResolver` provides the DEK to be used for encrypting the field.
|
||||
Since the `@ExplicitlyEncrypted` annotation does not need to specify an alt key name the `EncryptionKeyResolver` receives the current `EncryptionContext` that provides access to the field for dynamic DEK resolution.
|
||||
`EncryptionKeyResolver.annotationBased(...)` offers an implementation that will lookup values from the `@ExplicitlyEncrypted` annotation before falling back to the context based resolution.
|
||||
The converter setup for `MongoEncryptionConverter` requires a few steps as several components are involved.
|
||||
The bean setup consists of the following:
|
||||
|
||||
1. The `ClientEncryption` engine
|
||||
2. A `MongoEncryptionConverter` instance configured with `ClientEncryption` and a `EncryptionKeyResolver`.
|
||||
3. A `PropertyValueConverterFactory` that uses the registered `MongoEncryptionConverter` bean.
|
||||
|
||||
A side effect of using annotated key resolution is that the `@ExplicitEncrypted` annotation does not need to specify an alt key name.
|
||||
The `EncryptionKeyResolver` uses an `EncryptionContext` providing access to the property allowing for dynamic DEK resolution.
|
||||
|
||||
.Sample MongoEncryptionConverter Configuration
|
||||
====
|
||||
@@ -125,32 +133,38 @@ Since the `@ExplicitlyEncrypted` annotation does not need to specify an alt key
|
||||
----
|
||||
class Config extends AbstractMongoClientConfiguration {
|
||||
|
||||
// ...
|
||||
|
||||
@Autowired ApplicationContext appContext;
|
||||
|
||||
@Bean
|
||||
MongoEncryptionConverter encryptingConverter() {
|
||||
@Bean
|
||||
ClientEncryption clientEncryption() { <1>
|
||||
ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder();
|
||||
// …
|
||||
|
||||
ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder()
|
||||
// ...
|
||||
return ClientEncryptions.create(encryptionSettings);
|
||||
}
|
||||
|
||||
Encryption<BsonValue, BsonBinary> encryption = MongoClientEncryption.just(ClientEncryptions.create(encryptionSettings)) <1>
|
||||
EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotationBased((ctx) -> ...); <2>
|
||||
@Bean
|
||||
MongoEncryptionConverter encryptingConverter(ClientEncryption clientEncryption) {
|
||||
|
||||
return new MongoEncryptionConverter(encryption, keyResolver); <3>
|
||||
}
|
||||
Encryption<BsonValue, BsonBinary> encryption = MongoClientEncryption.just(clientEncryption);
|
||||
EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotated((ctx) -> …); <2>
|
||||
|
||||
return new MongoEncryptionConverter(encryption, keyResolver); <3>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureConverters(MongoConverterConfigurationAdapter adapter) {
|
||||
|
||||
adapter
|
||||
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(appContext)); <4>
|
||||
adapter
|
||||
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(appContext)); <4>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Set up a `com.mongodb.client.vault.ClientEncryption` specific `Encryption` engine.
|
||||
<2> Read the `EncryptionKey` from annotations on the field.
|
||||
|
||||
<1> Set up a `Encryption` engine using `com.mongodb.client.vault.ClientEncryption`.
|
||||
The instance is stateful and must be closed after usage.
|
||||
Spring takes care of this because `ClientEncryption` is ``Closeable``.
|
||||
<2> Set up an annotation-based `EncryptionKeyResolver` to determine the `EncryptionKey` from annotations.
|
||||
<3> Create the `MongoEncryptionConverter`.
|
||||
<4> Enable for a `PropertyValueConverter` within the `BeanFactory`.
|
||||
<4> Enable for a `PropertyValueConverter` lookup from the `BeanFactory`.
|
||||
====
|
||||
|
||||
Reference in New Issue
Block a user