Add support for explicit field encryption.
We now support explicit field encryption using mapped entities through the `@ExplicitEncrypted` annotation.
class Person {
ObjectId id;
@ExplicitEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic, altKeyName = "my-secret-key")
String socialSecurityNumber;
}
Encryption is applied transparently to all mapped entities leveraging the existing converter infrastructure.
Original pull request: #4302
Closes: #4284
This commit is contained in:
committed by
Mark Paluch
parent
3b7b1ace8b
commit
3b33f90e5c
@@ -112,6 +112,13 @@
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.mongodb</groupId>
|
||||
<artifactId>mongodb-crypt</artifactId>
|
||||
<version>1.6.1</version>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.projectreactor</groupId>
|
||||
<artifactId>reactor-core</artifactId>
|
||||
|
||||
@@ -68,6 +68,8 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
|
||||
private static final Set<String> DATA_INTEGRITY_EXCEPTIONS = new HashSet<>(
|
||||
Arrays.asList("WriteConcernException", "MongoWriteException", "MongoBulkWriteException"));
|
||||
|
||||
private static final Set<String> SECURITY_EXCEPTIONS = Set.of("MongoCryptException");
|
||||
|
||||
@Nullable
|
||||
public DataAccessException translateExceptionIfPossible(RuntimeException ex) {
|
||||
|
||||
@@ -131,6 +133,8 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator
|
||||
return new ClientSessionException(ex.getMessage(), ex);
|
||||
} else if (MongoDbErrorCodes.isTransactionFailureCode(code)) {
|
||||
return new MongoTransactionException(ex.getMessage(), ex);
|
||||
} else if(ex.getCause() != null && SECURITY_EXCEPTIONS.contains(ClassUtils.getShortName(ex.getCause().getClass()))) {
|
||||
return new PermissionDeniedDataAccessException(ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
return new UncategorizedMongoDbException(ex.getMessage(), ex);
|
||||
|
||||
@@ -331,7 +331,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
PersistentPropertyAccessor<?> convertingAccessor = PropertyTranslatingPropertyAccessor
|
||||
.create(new ConvertingPropertyAccessor<>(accessor, conversionService), propertyTranslator);
|
||||
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor,
|
||||
evaluator);
|
||||
evaluator, spELContext);
|
||||
|
||||
readProperties(context, entity, convertingAccessor, documentAccessor, valueProvider, evaluator,
|
||||
Predicates.isTrue());
|
||||
@@ -367,7 +367,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
populateProperties(context, mappedEntity, documentAccessor, evaluator, instance);
|
||||
|
||||
PersistentPropertyAccessor<?> convertingAccessor = new ConvertingPropertyAccessor<>(accessor, conversionService);
|
||||
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator);
|
||||
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(context, documentAccessor, evaluator, spELContext);
|
||||
|
||||
readProperties(context, mappedEntity, convertingAccessor, documentAccessor, valueProvider, evaluator,
|
||||
Predicates.isTrue());
|
||||
@@ -529,7 +529,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
ConversionContext contextToUse = context.withPath(currentPath);
|
||||
|
||||
MongoDbPropertyValueProvider valueProvider = new MongoDbPropertyValueProvider(contextToUse, documentAccessor,
|
||||
evaluator);
|
||||
evaluator, spELContext);
|
||||
|
||||
Predicate<MongoPersistentProperty> propertyFilter = isIdentifier(entity).or(isConstructorArgument(entity)).negate();
|
||||
readProperties(contextToUse, entity, accessor, documentAccessor, valueProvider, evaluator, propertyFilter);
|
||||
@@ -868,9 +868,9 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
dbObjectAccessor.put(prop, null);
|
||||
}
|
||||
} else if (!conversions.isSimpleType(value.getClass())) {
|
||||
writePropertyInternal(value, dbObjectAccessor, prop);
|
||||
writePropertyInternal(value, dbObjectAccessor, prop, accessor);
|
||||
} else {
|
||||
writeSimpleInternal(value, bson, prop);
|
||||
writeSimpleInternal(value, bson, prop, accessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -887,11 +887,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
return;
|
||||
}
|
||||
|
||||
writePropertyInternal(value, dbObjectAccessor, inverseProp);
|
||||
writePropertyInternal(value, dbObjectAccessor, inverseProp, accessor);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked" })
|
||||
protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor accessor, MongoPersistentProperty prop) {
|
||||
protected void writePropertyInternal(@Nullable Object obj, DocumentAccessor accessor, MongoPersistentProperty prop, PersistentPropertyAccessor<?> persistentPropertyAccessor) {
|
||||
|
||||
if (obj == null) {
|
||||
return;
|
||||
@@ -902,7 +902,13 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
if (conversions.hasValueConverter(prop)) {
|
||||
accessor.put(prop, conversions.getPropertyValueConversions().getValueConverter(prop).write(obj,
|
||||
new MongoConversionContext(prop, this)));
|
||||
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public <T> T getPropertyValue(MongoPersistentProperty property) {
|
||||
return (T) persistentPropertyAccessor.getProperty(property);
|
||||
}
|
||||
}, prop, this, spELContext)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1234,12 +1240,18 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
BsonUtils.addToMap(bson, key, getPotentiallyConvertedSimpleWrite(value, Object.class));
|
||||
}
|
||||
|
||||
private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property) {
|
||||
private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersistentProperty property, PersistentPropertyAccessor<?> persistentPropertyAccessor) {
|
||||
DocumentAccessor accessor = new DocumentAccessor(bson);
|
||||
|
||||
if (conversions.hasValueConverter(property)) {
|
||||
accessor.put(property, conversions.getPropertyValueConversions().getValueConverter(property).write(value,
|
||||
new MongoConversionContext(property, this)));
|
||||
new MongoConversionContext(new PropertyValueProvider<MongoPersistentProperty>() {
|
||||
@Nullable
|
||||
@Override
|
||||
public <T> T getPropertyValue(MongoPersistentProperty property) {
|
||||
return (T) persistentPropertyAccessor.getProperty(property);
|
||||
}
|
||||
}, property, this, spELContext)));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1845,6 +1857,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
final ConversionContext context;
|
||||
final DocumentAccessor accessor;
|
||||
final SpELExpressionEvaluator evaluator;
|
||||
final SpELContext spELContext;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MongoDbPropertyValueProvider} for the given source, {@link SpELExpressionEvaluator} and
|
||||
@@ -1855,7 +1868,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
* @param evaluator must not be {@literal null}.
|
||||
*/
|
||||
MongoDbPropertyValueProvider(ConversionContext context, Bson source, SpELExpressionEvaluator evaluator) {
|
||||
this(context, new DocumentAccessor(source), evaluator);
|
||||
this(context, new DocumentAccessor(source), evaluator, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1867,7 +1880,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
* @param evaluator must not be {@literal null}.
|
||||
*/
|
||||
MongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor accessor,
|
||||
SpELExpressionEvaluator evaluator) {
|
||||
SpELExpressionEvaluator evaluator, SpELContext spELContext) {
|
||||
|
||||
Assert.notNull(context, "ConversionContext must no be null");
|
||||
Assert.notNull(accessor, "DocumentAccessor must no be null");
|
||||
@@ -1876,6 +1889,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
this.context = context;
|
||||
this.accessor = accessor;
|
||||
this.evaluator = evaluator;
|
||||
this.spELContext = spELContext;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -1892,7 +1906,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
CustomConversions conversions = context.getCustomConversions();
|
||||
if (conversions.hasValueConverter(property)) {
|
||||
return (T) conversions.getPropertyValueConversions().getValueConverter(property).read(value,
|
||||
new MongoConversionContext(property, context.getSourceConverter()));
|
||||
new MongoConversionContext(this, property, context.getSourceConverter(), spELContext));
|
||||
}
|
||||
|
||||
ConversionContext contextToUse = context.forProperty(property);
|
||||
@@ -1902,7 +1916,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
|
||||
public MongoDbPropertyValueProvider withContext(ConversionContext context) {
|
||||
|
||||
return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator);
|
||||
return context == this.context ? this : new MongoDbPropertyValueProvider(context, accessor, evaluator, spELContext);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1925,7 +1939,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
|
||||
*/
|
||||
AssociationAwareMongoDbPropertyValueProvider(ConversionContext context, DocumentAccessor source,
|
||||
SpELExpressionEvaluator evaluator) {
|
||||
super(context, source, evaluator);
|
||||
super(context, source, evaluator, MappingMongoConverter.this.spELContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -15,10 +15,17 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.convert;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.bson.conversions.Bson;
|
||||
import org.springframework.data.convert.ValueConversionContext;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.model.PropertyValueProvider;
|
||||
import org.springframework.data.mapping.model.SpELContext;
|
||||
import org.springframework.data.mapping.model.SpELExpressionEvaluator;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -29,13 +36,23 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
public class MongoConversionContext implements ValueConversionContext<MongoPersistentProperty> {
|
||||
|
||||
private final PropertyValueProvider accessor; // TODO: generics
|
||||
private final MongoPersistentProperty persistentProperty;
|
||||
private final MongoConverter mongoConverter;
|
||||
|
||||
public MongoConversionContext(MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
|
||||
@Nullable
|
||||
private final SpELContext spELContext;
|
||||
|
||||
public MongoConversionContext(PropertyValueProvider<?> accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) {
|
||||
this(accessor, persistentProperty, mongoConverter, null);
|
||||
}
|
||||
|
||||
public MongoConversionContext(PropertyValueProvider<?> accessor, MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, SpELContext spELContext) {
|
||||
|
||||
this.accessor = accessor;
|
||||
this.persistentProperty = persistentProperty;
|
||||
this.mongoConverter = mongoConverter;
|
||||
this.spELContext = spELContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -43,6 +60,10 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
|
||||
return persistentProperty;
|
||||
}
|
||||
|
||||
public Object getValue(String propertyPath) {
|
||||
return accessor.getPropertyValue(persistentProperty.getOwner().getRequiredPersistentProperty(propertyPath));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
|
||||
return (T) mongoConverter.convertToMongoType(value, target);
|
||||
@@ -53,4 +74,9 @@ public class MongoConversionContext implements ValueConversionContext<MongoPersi
|
||||
return value instanceof Bson ? mongoConverter.read(target.getType(), (Bson) value)
|
||||
: ValueConversionContext.super.read(value, target);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public SpELContext getSpELContext() {
|
||||
return spELContext;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,7 +439,7 @@ public class QueryMapper {
|
||||
&& converter.getCustomConversions().hasValueConverter(documentField.getProperty())) {
|
||||
return converter.getCustomConversions().getPropertyValueConversions()
|
||||
.getValueConverter(documentField.getProperty())
|
||||
.write(value, new MongoConversionContext(documentField.getProperty(), converter));
|
||||
.write(value, new MongoConversionContext(null, documentField.getProperty(), converter));
|
||||
}
|
||||
|
||||
if (documentField.isIdField() && !documentField.isAssociation()) {
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.convert.encryption;
|
||||
|
||||
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
|
||||
import org.springframework.data.mongodb.core.convert.MongoValueConverter;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
|
||||
|
||||
/**
|
||||
* A specialized {@link MongoValueConverter} for {@literal en-/decrypting} properties.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface EncryptingConverter<S, T> extends MongoValueConverter<S, T> {
|
||||
|
||||
@Override
|
||||
default S read(Object value, MongoConversionContext context) {
|
||||
return decrypt(value, buildEncryptionContext(context));
|
||||
}
|
||||
|
||||
@Override
|
||||
default T write(Object value, MongoConversionContext context) {
|
||||
return encrypt(value, buildEncryptionContext(context));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt the given encrypted source value within the given {@link EncryptionContext context}.
|
||||
*
|
||||
* @param encryptedValue the encrypted source.
|
||||
* @param context the context to operate in.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
S decrypt(Object encryptedValue, EncryptionContext context);
|
||||
|
||||
/**
|
||||
* Encrypt the given raw source value within the given {@link EncryptionContext context}.
|
||||
*
|
||||
* @param value the encrypted source.
|
||||
* @param context the context to operate in.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
T encrypt(Object value, EncryptionContext context);
|
||||
|
||||
/**
|
||||
* Obtain the {@link EncryptionContext} for a given {@link MongoConversionContext value conversion context}.
|
||||
*
|
||||
* @param context the current MongoDB specific {@link org.springframework.data.convert.ValueConversionContext}.
|
||||
* @return the {@link EncryptionContext} to operate in.
|
||||
* @see org.springframework.data.convert.ValueConversionContext
|
||||
*/
|
||||
EncryptionContext buildEncryptionContext(MongoConversionContext context);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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.convert.encryption;
|
||||
|
||||
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Default {@link EncryptionContext} implementation.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
class ExplicitEncryptionContext implements EncryptionContext {
|
||||
|
||||
private final MongoConversionContext conversionContext;
|
||||
|
||||
public ExplicitEncryptionContext(MongoConversionContext conversionContext) {
|
||||
this.conversionContext = conversionContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MongoPersistentProperty getProperty() {
|
||||
return conversionContext.getProperty();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object lookupValue(String path) {
|
||||
return conversionContext.getValue(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object convertToMongoType(Object value) {
|
||||
return conversionContext.write(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EvaluationContext getEvaluationContext(Object source) {
|
||||
return conversionContext.getSpELContext().getEvaluationContext(source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T read(@Nullable Object value, TypeInformation<T> target) {
|
||||
return conversionContext.read(value, target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T write(@Nullable Object value, TypeInformation<T> target) {
|
||||
return conversionContext.write(value, target);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
* 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.convert.encryption;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.bson.BsonArray;
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.BsonDocument;
|
||||
import org.bson.BsonValue;
|
||||
import org.bson.Document;
|
||||
import org.bson.types.Binary;
|
||||
import org.springframework.core.CollectionFactory;
|
||||
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
|
||||
import org.springframework.data.mongodb.core.encryption.Encryption;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionContext;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionOptions;
|
||||
import org.springframework.data.mongodb.core.mapping.Encrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.util.BsonUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public class MongoEncryptionConverter implements EncryptingConverter<Object, Object> {
|
||||
|
||||
private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class);
|
||||
|
||||
private Encryption<BsonValue, BsonBinary> encryption;
|
||||
private final EncryptionKeyResolver keyResolver;
|
||||
|
||||
public MongoEncryptionConverter(Encryption<BsonValue, BsonBinary> encryption, EncryptionKeyResolver keyResolver) {
|
||||
|
||||
this.encryption = encryption;
|
||||
this.keyResolver = keyResolver;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Object read(Object value, MongoConversionContext context) {
|
||||
|
||||
Object decrypted = EncryptingConverter.super.read(value, context);
|
||||
return decrypted instanceof BsonValue ? BsonUtils.toJavaType((BsonValue) decrypted) : decrypted;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decrypt(Object encryptedValue, EncryptionContext context) {
|
||||
|
||||
Object decryptedValue = encryptedValue;
|
||||
if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) {
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("Decrypting %s.%s.", getProperty(context).getOwner().getName(),
|
||||
getProperty(context).getName()));
|
||||
}
|
||||
|
||||
decryptedValue = encryption.decrypt((BsonBinary) BsonUtils.simpleToBsonValue(encryptedValue));
|
||||
|
||||
// in case the driver has auto decryption (aka .bypassAutoEncryption(true)) active
|
||||
// https://github.com/mongodb/mongo-java-driver/blob/master/driver-sync/src/examples/tour/ClientSideEncryptionExplicitEncryptionOnlyTour.java
|
||||
if (encryptedValue == decryptedValue) {
|
||||
return decryptedValue;
|
||||
}
|
||||
}
|
||||
|
||||
MongoPersistentProperty persistentProperty = getProperty(context);
|
||||
|
||||
if (getProperty(context).isCollectionLike() && decryptedValue instanceof Iterable<?> iterable) {
|
||||
if (!persistentProperty.isEntity()) {
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), 10);
|
||||
iterable.forEach(it -> collection.add(BsonUtils.toJavaType((BsonValue) it)));
|
||||
return collection;
|
||||
} else {
|
||||
Collection<Object> collection = CollectionFactory.createCollection(persistentProperty.getType(), 10);
|
||||
iterable.forEach(it -> {
|
||||
collection.add(context.read(BsonUtils.toJavaType((BsonValue) it),
|
||||
persistentProperty.getActualType()));
|
||||
});
|
||||
return collection;
|
||||
}
|
||||
}
|
||||
|
||||
if (!persistentProperty.isEntity() && decryptedValue instanceof BsonValue bsonValue) {
|
||||
if (persistentProperty.isMap() && persistentProperty.getType() != Document.class) {
|
||||
return new LinkedHashMap<>((Document) BsonUtils.toJavaType(bsonValue));
|
||||
|
||||
}
|
||||
return BsonUtils.toJavaType(bsonValue);
|
||||
}
|
||||
|
||||
if (persistentProperty.isEntity() && decryptedValue instanceof BsonDocument bsonDocument) {
|
||||
return context.read(BsonUtils.toJavaType(bsonDocument),
|
||||
persistentProperty.getTypeInformation().getType());
|
||||
}
|
||||
|
||||
return decryptedValue;
|
||||
}
|
||||
|
||||
private MongoPersistentProperty getProperty(EncryptionContext context) {
|
||||
return context.getProperty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object encrypt(Object value, EncryptionContext context) {
|
||||
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug(String.format("Encrypting %s.%s.", getProperty(context).getOwner().getName(),
|
||||
getProperty(context).getName()));
|
||||
}
|
||||
|
||||
MongoPersistentProperty persistentProperty = getProperty(context);
|
||||
|
||||
Encrypted annotation = persistentProperty.findAnnotation(Encrypted.class);
|
||||
if(annotation == null) {
|
||||
annotation = persistentProperty.getOwner().findAnnotation(Encrypted.class);
|
||||
}
|
||||
|
||||
EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm());
|
||||
encryptionOptions.setKey(keyResolver.getKey(context));
|
||||
|
||||
if (!persistentProperty.isEntity()) {
|
||||
|
||||
if (persistentProperty.isCollectionLike()) {
|
||||
return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions);
|
||||
}
|
||||
if (persistentProperty.isMap()) {
|
||||
Object convertedMap = context.write(value);
|
||||
if (convertedMap instanceof Document document) {
|
||||
return encryption.encrypt(document.toBsonDocument(), encryptionOptions);
|
||||
}
|
||||
}
|
||||
return encryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions);
|
||||
}
|
||||
if (persistentProperty.isCollectionLike()) {
|
||||
return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions);
|
||||
}
|
||||
|
||||
Object write = context.write(value);
|
||||
if (write instanceof Document doc) {
|
||||
return encryption.encrypt(doc.toBsonDocument(), encryptionOptions);
|
||||
}
|
||||
return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions);
|
||||
}
|
||||
|
||||
public BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property,
|
||||
EncryptionContext context) {
|
||||
|
||||
BsonArray bsonArray = new BsonArray();
|
||||
if (!property.isEntity()) {
|
||||
if (value instanceof Collection values) {
|
||||
values.forEach(it -> bsonArray.add(BsonUtils.simpleToBsonValue(it)));
|
||||
} else if (ObjectUtils.isArray(value)) {
|
||||
for (Object o : ObjectUtils.toObjectArray(value)) {
|
||||
bsonArray.add(BsonUtils.simpleToBsonValue(o));
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
public EncryptionContext buildEncryptionContext(MongoConversionContext context) {
|
||||
return new ExplicitEncryptionContext(context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Component responsible for en-/decrypting values.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface Encryption<S, T> {
|
||||
|
||||
/**
|
||||
* Encrypt the given value.
|
||||
*
|
||||
* @param value must not be {@literal null}.
|
||||
* @param options must not be {@literal null}.
|
||||
* @return the encrypted value.
|
||||
*/
|
||||
T encrypt(S value, EncryptionOptions options);
|
||||
|
||||
/**
|
||||
* Decrypt the given value.
|
||||
*
|
||||
* @param value must not be {@literal null}.
|
||||
* @return the decrypted value.
|
||||
*/
|
||||
S decrypt(T value);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import org.springframework.data.convert.ValueConversionContext;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
public interface EncryptionContext {
|
||||
|
||||
/**
|
||||
* Returns the {@link MongoPersistentProperty} to be handled.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
MongoPersistentProperty getProperty();
|
||||
|
||||
/**
|
||||
* Shortcut for converting a given {@literal value} into its store representation using the root
|
||||
* {@link ValueConversionContext}.
|
||||
*
|
||||
* @param value
|
||||
* @return
|
||||
*/
|
||||
Object convertToMongoType(Object value);
|
||||
|
||||
/**
|
||||
* Reads the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}.
|
||||
*
|
||||
* @param value {@link Object value} to be read; can be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
* @throws IllegalStateException if value cannot be read as an instance of {@link Class type}.
|
||||
*/
|
||||
default <T> T read(@Nullable Object value) {
|
||||
return (T) read(value, getProperty().getTypeInformation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the value as an instance of {@link Class type}.
|
||||
*
|
||||
* @param value {@link Object value} to be read; can be {@literal null}.
|
||||
* @param target {@link Class type} of value to be read; must not be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
* @throws IllegalStateException if value cannot be read as an instance of {@link Class type}.
|
||||
*/
|
||||
default <T> T read(@Nullable Object value, Class<T> target) {
|
||||
return read(value, TypeInformation.of(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the value as an instance of {@link TypeInformation type}.
|
||||
*
|
||||
* @param value {@link Object value} to be read; can be {@literal null}.
|
||||
* @param target {@link TypeInformation type} of value to be read; must not be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
* @throws IllegalStateException if value cannot be read as an instance of {@link Class type}.
|
||||
*/
|
||||
<T> T read(@Nullable Object value, TypeInformation<T> target);
|
||||
|
||||
/**
|
||||
* Write the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}.
|
||||
*
|
||||
* @param value {@link Object value} to write; can be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
* @throws IllegalStateException if value cannot be written as an instance of the
|
||||
* {@link PersistentProperty#getTypeInformation() property type}.
|
||||
* @see PersistentProperty#getTypeInformation()
|
||||
* @see #write(Object, TypeInformation)
|
||||
*/
|
||||
@Nullable
|
||||
default <T> T write(@Nullable Object value) {
|
||||
return (T) write(value, getProperty().getTypeInformation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the value as an instance of {@link Class type}.
|
||||
*
|
||||
* @param value {@link Object value} to write; can be {@literal null}.
|
||||
* @param target {@link Class type} of value to be written; must not be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
* @throws IllegalStateException if value cannot be written as an instance of {@link Class type}.
|
||||
*/
|
||||
@Nullable
|
||||
default <T> T write(@Nullable Object value, Class<T> target) {
|
||||
return write(value, TypeInformation.of(target));
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the value as an instance of given {@link TypeInformation type}.
|
||||
*
|
||||
* @param value {@link Object value} to write; can be {@literal null}.
|
||||
* @param target {@link TypeInformation type} of value to be written; must not be {@literal null}.
|
||||
* @return can be {@literal null}.
|
||||
* @throws IllegalStateException if value cannot be written as an instance of {@link Class type}.
|
||||
*/
|
||||
@Nullable
|
||||
<T> T write(@Nullable Object value, TypeInformation<T> target);
|
||||
|
||||
/**
|
||||
* Lookup the value for a given path within the current context.
|
||||
*
|
||||
* @param path the path/property name to resolve the current value for.
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
@Nullable
|
||||
Object lookupValue(String path);
|
||||
|
||||
EvaluationContext getEvaluationContext(Object source);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* The {@link EncryptionKey} represents a {@literal Data Encryption Key} reference that can be either direct via the
|
||||
* {@link KeyId key id} or its {@link AltKeyName Key Alternative Name}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public interface EncryptionKey {
|
||||
|
||||
/**
|
||||
* @return the value that allows to reference a specific key
|
||||
*/
|
||||
Object value();
|
||||
|
||||
/**
|
||||
* @return the {@link Type} of reference.
|
||||
*/
|
||||
Type type();
|
||||
|
||||
/**
|
||||
* Create a new {@link EncryptionKey} that uses the keys id for reference.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @return new instance of {@link KeyId}.
|
||||
*/
|
||||
static KeyId keyId(BsonBinary key) {
|
||||
return new KeyId(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link EncryptionKey} that uses an {@literal Key Alternative Name} for reference.
|
||||
*
|
||||
* @param altKeyName must not be {@literal null}.
|
||||
* @return new instance of {@link KeyId}.
|
||||
*/
|
||||
static AltKeyName altKeyName(String altKeyName) {
|
||||
return new AltKeyName(altKeyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value must not be {@literal null}.
|
||||
*/
|
||||
record KeyId(BsonBinary value) implements EncryptionKey {
|
||||
|
||||
@Override
|
||||
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.
|
||||
*/
|
||||
enum Type {
|
||||
|
||||
/**
|
||||
* Key referenced via its {@literal id}.
|
||||
*/
|
||||
ID,
|
||||
|
||||
/**
|
||||
* Key referenced via an {@literal Key Alternative Name}.
|
||||
*/
|
||||
ALT
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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.types.Binary;
|
||||
import org.springframework.data.mongodb.core.mapping.Encrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.util.BsonUtils;
|
||||
import org.springframework.data.mongodb.util.encryption.EncryptionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
* @see EncryptionKey
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface EncryptionKeyResolver {
|
||||
|
||||
/**
|
||||
* Get the {@link EncryptionKey Data Encryption Key}.
|
||||
*
|
||||
* @param encryptionContext the current {@link EncryptionContext context}.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
EncryptionKey getKey(EncryptionContext encryptionContext);
|
||||
|
||||
/**
|
||||
* Obtain an {@link EncryptionKeyResolver} that evaluates {@link ExplicitlyEncrypted#altKeyName()} and only calls the
|
||||
* fallback {@link EncryptionKeyResolver resolver} if no {@literal Key Alternate Name} is present.
|
||||
*
|
||||
* @param fallback must not be {@literal null}.
|
||||
* @return new instance of {@link EncryptionKeyResolver}.
|
||||
*/
|
||||
static EncryptionKeyResolver annotationBased(EncryptionKeyResolver fallback) {
|
||||
|
||||
return ((encryptionContext) -> {
|
||||
|
||||
ExplicitlyEncrypted annotation = encryptionContext.getProperty().findAnnotation(ExplicitlyEncrypted.class);
|
||||
if (annotation == null || !StringUtils.hasText(annotation.altKeyName())) {
|
||||
|
||||
Encrypted encrypted = encryptionContext.getProperty().getOwner().findAnnotation(Encrypted.class);
|
||||
if (encrypted == null) {
|
||||
return fallback.getKey(encryptionContext);
|
||||
}
|
||||
|
||||
Object o = EncryptionUtils.resolveKeyId(encrypted.keyId()[0],
|
||||
() -> encryptionContext.getEvaluationContext(new Object()));
|
||||
if (o instanceof BsonBinary binary) {
|
||||
return EncryptionKey.keyId(binary);
|
||||
}
|
||||
if (o instanceof Binary binary) {
|
||||
return EncryptionKey.keyId((BsonBinary) BsonUtils.simpleToBsonValue(binary));
|
||||
}
|
||||
if (o instanceof String string) {
|
||||
return EncryptionKey.altKeyName(string);
|
||||
}
|
||||
}
|
||||
|
||||
String altKeyName = annotation.altKeyName();
|
||||
if (altKeyName.startsWith("/")) {
|
||||
Object fieldValue = encryptionContext.lookupValue(altKeyName.replace("/", ""));
|
||||
if (fieldValue == null) {
|
||||
throw new IllegalStateException(String.format("Key Alternative Name for %s was null", altKeyName));
|
||||
}
|
||||
return new EncryptionKey.AltKeyName(fieldValue.toString());
|
||||
} else {
|
||||
return new EncryptionKey.AltKeyName(altKeyName);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Options, like the {@link #algorithm()}, to apply when encrypting values.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public class EncryptionOptions {
|
||||
|
||||
private final String algorithm;
|
||||
private @Nullable EncryptionKey key;
|
||||
|
||||
public EncryptionOptions(String algorithm) {
|
||||
this.algorithm = algorithm;
|
||||
}
|
||||
|
||||
public EncryptionOptions setKey(EncryptionKey key) {
|
||||
|
||||
this.key = key;
|
||||
return this;
|
||||
}
|
||||
|
||||
public EncryptionKey key() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public String algorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
EncryptionOptions that = (EncryptionOptions) o;
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(algorithm, that.algorithm)) {
|
||||
return false;
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(key, that.key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = ObjectUtils.nullSafeHashCode(algorithm);
|
||||
result = 31 * result + ObjectUtils.nullSafeHashCode(key);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.BsonValue;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import com.mongodb.client.model.vault.EncryptOptions;
|
||||
import com.mongodb.client.vault.ClientEncryption;
|
||||
|
||||
/**
|
||||
* {@link ClientEncryption} based {@link Encryption} implementation.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
*/
|
||||
public class MongoClientEncryption implements Encryption<BsonValue, BsonBinary> {
|
||||
|
||||
private final Supplier<ClientEncryption> source;
|
||||
private final AtomicReference<ClientEncryption> cached;
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link MongoClientEncryption} instance for the given {@link ClientEncryption}.
|
||||
*
|
||||
* @param clientEncryption must not be {@literal null}.
|
||||
* @return new instance of {@link MongoClientEncryption}.
|
||||
*/
|
||||
public static MongoClientEncryption just(ClientEncryption clientEncryption) {
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public BsonValue decrypt(BsonBinary value) {
|
||||
return getClientEncryption().decrypt(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BsonBinary encrypt(BsonValue value, EncryptionOptions options) {
|
||||
|
||||
EncryptOptions encryptOptions = new EncryptOptions(options.algorithm());
|
||||
|
||||
if (Type.ALT.equals(options.key().type())) {
|
||||
encryptOptions = encryptOptions.keyAltName(options.key().value().toString());
|
||||
} else {
|
||||
encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value());
|
||||
}
|
||||
|
||||
return getClientEncryption().encrypt(value, encryptOptions);
|
||||
}
|
||||
|
||||
public ClientEncryption getClientEncryption() {
|
||||
return cached.get();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* 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.mapping;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.data.convert.PropertyValueConverter;
|
||||
import org.springframework.data.convert.ValueConverter;
|
||||
import org.springframework.data.mongodb.core.convert.encryption.EncryptingConverter;
|
||||
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
|
||||
|
||||
/**
|
||||
* {@link ExplicitlyEncrypted} is a {@link ElementType#FIELD field} level {@link ValueConverter} annotation that
|
||||
* 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
|
||||
* of the encryption process.
|
||||
*
|
||||
* <pre class="code">
|
||||
* public class Patient {
|
||||
* private ObjectId id;
|
||||
* private String name;
|
||||
*
|
||||
* @ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "secred-key-alternative-name") //
|
||||
* private String ssn;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.1
|
||||
* @see ValueConverter
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.FIELD)
|
||||
@Encrypted
|
||||
@ValueConverter
|
||||
public @interface ExplicitlyEncrypted {
|
||||
|
||||
/**
|
||||
* Define the algorithm to use.
|
||||
* <p>
|
||||
* A {@literal Deterministic} algorithm ensures that a given input value always encrypts to the same output while a
|
||||
* {@literal randomized} one will produce different results every time.
|
||||
* <p>
|
||||
* Please make sure to use an algorithm that is in line with MongoDB's encryption rules for simple types, complex
|
||||
* objects and arrays as well as the query limitations that come with each of them.
|
||||
*
|
||||
* @return the string representation of the encryption algorithm to use.
|
||||
*/
|
||||
@AliasFor(annotation = Encrypted.class, value = "algorithm")
|
||||
String algorithm() default "";
|
||||
|
||||
/**
|
||||
* Set the {@literal Key Alternate Name} that references the {@literal Data Encryption Key} to be used.
|
||||
* <p>
|
||||
* An empty String indicates that no alternative key name was configured.
|
||||
* <p>
|
||||
* It is possible to use the {@literal "/"} character as a prefix to access a particular field value in the same
|
||||
* domain type. In this case {@code "/name"} references the value of the {@literal name} field. Please note that
|
||||
* update operations will require the full object to resolve those values.
|
||||
*
|
||||
* @return the {@literal Key Alternate Name} if set or an empty {@link String}.
|
||||
*/
|
||||
String altKeyName() default "";
|
||||
|
||||
/**
|
||||
* The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property.
|
||||
*
|
||||
* @return the configured {@link EncryptingConverter}. A {@link MongoEncryptionConverter} by default.
|
||||
*/
|
||||
@AliasFor(annotation = ValueConverter.class, value = "value")
|
||||
Class<? extends PropertyValueConverter> value() default MongoEncryptionConverter.class;
|
||||
}
|
||||
@@ -38,6 +38,7 @@ import org.bson.codecs.DocumentCodec;
|
||||
import org.bson.codecs.configuration.CodecRegistry;
|
||||
import org.bson.conversions.Bson;
|
||||
import org.bson.json.JsonParseException;
|
||||
import org.bson.types.Binary;
|
||||
import org.bson.types.ObjectId;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.mongodb.CodecRegistryProvider;
|
||||
@@ -370,6 +371,10 @@ public class BsonUtils {
|
||||
return new BsonDouble((Float) source);
|
||||
}
|
||||
|
||||
if(source instanceof Binary binary) {
|
||||
return new BsonBinary(binary.getType(), binary.getData());
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(String.format("Unable to convert %s (%s) to BsonValue.", source,
|
||||
source != null ? source.getClass().getName() : "null"));
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.util.encryption;
|
||||
|
||||
import java.util.Base64;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@@ -67,7 +68,7 @@ public final class EncryptionUtils {
|
||||
new BsonBinary(UUID.fromString(potentialKeyId.toString())).getData());
|
||||
} catch (IllegalArgumentException e) {
|
||||
|
||||
return new Binary(BsonBinarySubType.UUID_STANDARD, Base64Utils.decodeFromString(potentialKeyId.toString()));
|
||||
return new Binary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode(potentialKeyId.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2613,7 +2613,7 @@ class MappingMongoConverterUnitTests {
|
||||
doReturn(Person.class).when(persistentProperty).getType();
|
||||
doReturn(Person.class).when(persistentProperty).getRawType();
|
||||
|
||||
converter.writePropertyInternal(sourceValue, accessor, persistentProperty);
|
||||
converter.writePropertyInternal(sourceValue, accessor, persistentProperty, null);
|
||||
|
||||
assertThat(accessor.getDocument())
|
||||
.isEqualTo(new org.bson.Document("pName", new org.bson.Document("_id", id.toString())));
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.BsonBinarySubType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.mongodb.core.mapping.Encrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class EncryptionKeyResolverUnitTests {
|
||||
|
||||
@Mock //
|
||||
EncryptionKeyResolver fallbackKeyResolver;
|
||||
|
||||
MongoTestMappingContext mappingContext = MongoTestMappingContext.newTestContext().init();
|
||||
|
||||
EncryptionKey defaultEncryptionKey = EncryptionKey
|
||||
.keyId(new BsonBinary("super-secret".getBytes(StandardCharsets.UTF_8)));
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
when(fallbackKeyResolver.getKey(any())).thenReturn(defaultEncryptionKey);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void usesDefaultKeyIfNoAnnotationPresent() {
|
||||
|
||||
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
|
||||
AnnotatedWithExplicitlyEncrypted::getNotAnnotated);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isSameAs(defaultEncryptionKey);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void usesDefaultKeyIfAnnotatedValueIsEmpty() {
|
||||
|
||||
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
|
||||
AnnotatedWithExplicitlyEncrypted::getAlgorithm);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isSameAs(defaultEncryptionKey);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void usesDefaultAltKeyNameIfPresent() {
|
||||
|
||||
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
|
||||
AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyName);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.altKeyName("sec-key-name"));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void readsAltKeyNameFromContextIfReferencingPropertyValue() {
|
||||
|
||||
EncryptionContext ctx = prepareEncryptionContext(AnnotatedWithExplicitlyEncrypted.class,
|
||||
AnnotatedWithExplicitlyEncrypted::getAlgorithmAndAltKeyNameFromPropertyValue);
|
||||
when(ctx.lookupValue(eq("notAnnotated"))).thenReturn("born-to-be-wild");
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.altKeyName("born-to-be-wild"));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void readsKeyIdFromEncryptedAnnotationIfNoBetterCandidateAvailable() {
|
||||
|
||||
EncryptionContext ctx = prepareEncryptionContext(
|
||||
AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType.class,
|
||||
AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType::getKeyIdFromDomainType);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.keyId(
|
||||
new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g=="))));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void ignoresKeyIdFromEncryptedAnnotationWhenBetterCandidateAvailable() {
|
||||
|
||||
EncryptionContext ctx = prepareEncryptionContext(KeyIdFromSpel.class, KeyIdFromSpel::getKeyIdFromDomainType);
|
||||
|
||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
|
||||
evaluationContext.setVariable("myKeyId", "xKVup8B1Q+CkHaVRx+qa+g==");
|
||||
|
||||
when(ctx.getEvaluationContext(any())).thenReturn(evaluationContext);
|
||||
|
||||
EncryptionKey key = EncryptionKeyResolver.annotationBased(fallbackKeyResolver).getKey(ctx);
|
||||
|
||||
assertThat(key).isEqualTo(EncryptionKey.keyId(
|
||||
new BsonBinary(BsonBinarySubType.UUID_STANDARD, Base64.getDecoder().decode("xKVup8B1Q+CkHaVRx+qa+g=="))));
|
||||
}
|
||||
|
||||
private <T> EncryptionContext prepareEncryptionContext(Class<T> type, Function<T, ?> property) {
|
||||
|
||||
EncryptionContext encryptionContext = mock(EncryptionContext.class);
|
||||
when(encryptionContext.getProperty()).thenReturn(mappingContext.getPersistentPropertyFor(type, property));
|
||||
return encryptionContext;
|
||||
}
|
||||
|
||||
@Data
|
||||
class AnnotatedWithExplicitlyEncrypted {
|
||||
|
||||
String notAnnotated;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
String algorithm;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") //
|
||||
String algorithmAndAltKeyName;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") //
|
||||
String algorithmAndAltKeyNameFromPropertyValue;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Encrypted(keyId = "xKVup8B1Q+CkHaVRx+qa+g==")
|
||||
class AnnotatedWithExplicitlyEncryptedHavingDefaultAlgorithmServedViaAnnotationOnType {
|
||||
|
||||
@ExplicitlyEncrypted //
|
||||
String keyIdFromDomainType;
|
||||
|
||||
@ExplicitlyEncrypted(altKeyName = "sec-key-name") //
|
||||
String altKeyNameFromPropertyIgnoringKeyIdFromDomainType;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Encrypted(keyId = "#{#myKeyId}")
|
||||
class KeyIdFromSpel {
|
||||
|
||||
@ExplicitlyEncrypted //
|
||||
String keyIdFromDomainType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.UuidRepresentation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
class EncryptionKeyUnitTests {
|
||||
|
||||
@Test // GH-4284
|
||||
void keyIdToStringDoesNotRevealEntireKey() {
|
||||
|
||||
UUID uuid = UUID.randomUUID();
|
||||
|
||||
assertThat(EncryptionKey.keyId(new BsonBinary(uuid, UuidRepresentation.STANDARD)).toString())
|
||||
.contains(uuid.toString().substring(0, 6) + "***");
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void altKeyNameToStringDoesNotRevealEntireKey() {
|
||||
|
||||
assertThat(EncryptionKey.altKeyName("s").toString()).contains("***");
|
||||
assertThat(EncryptionKey.altKeyName("su").toString()).contains("***");
|
||||
assertThat(EncryptionKey.altKeyName("sup").toString()).contains("***");
|
||||
assertThat(EncryptionKey.altKeyName("super-secret-key").toString()).contains("sup***");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
|
||||
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
|
||||
import static org.springframework.data.mongodb.core.query.Criteria.*;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.Document;
|
||||
import org.bson.types.Binary;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.dao.PermissionDeniedDataAccessException;
|
||||
import org.springframework.data.convert.PropertyValueConverterFactory;
|
||||
import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.aggregation.Aggregation;
|
||||
import org.springframework.data.mongodb.core.aggregation.AggregationResults;
|
||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
|
||||
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
|
||||
import org.springframework.data.mongodb.core.encryption.EncryptionTests.Config;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
import org.springframework.data.util.Lazy;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
import com.mongodb.ClientEncryptionSettings;
|
||||
import com.mongodb.ConnectionString;
|
||||
import com.mongodb.MongoClientSettings;
|
||||
import com.mongodb.MongoNamespace;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.model.Filters;
|
||||
import com.mongodb.client.model.IndexOptions;
|
||||
import com.mongodb.client.model.Indexes;
|
||||
import com.mongodb.client.model.vault.DataKeyOptions;
|
||||
import com.mongodb.client.vault.ClientEncryptions;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration(classes = Config.class)
|
||||
public class EncryptionTests {
|
||||
|
||||
@Autowired MongoTemplate template;
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.ssn = "mySecretSSN";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.address = new Address();
|
||||
source.address.city = "NYC";
|
||||
source.address.street = "4th Ave.";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptValueWithinComplexOne() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.encryptedZip = new AddressWithEncryptedZip();
|
||||
source.encryptedZip.city = "Boston";
|
||||
source.encryptedZip.street = "central square";
|
||||
source.encryptedZip.zip = "1234567890";
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> {
|
||||
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
|
||||
}) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptListOfSimpleValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.listOfString = Arrays.asList("spring", "data", "mongodb");
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("listOfString")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptListOfComplexValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
Address address = new Address();
|
||||
address.city = "SFO";
|
||||
address.street = "---";
|
||||
|
||||
source.listOfComplex = Collections.singletonList(address);
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("listOfComplex")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptMapOfSimpleValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.mapOfString = Map.of("k1", "v1", "k2", "v2");
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("mapOfString")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void enDeCryptMapOfComplexValues() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
Address address1 = new Address();
|
||||
address1.city = "SFO";
|
||||
address1.street = "---";
|
||||
|
||||
Address address2 = new Address();
|
||||
address2.city = "NYC";
|
||||
address2.street = "---";
|
||||
|
||||
source.mapOfComplex = Map.of("a1", address1, "a2", address2);
|
||||
|
||||
template.save(source);
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("mapOfComplex")).isInstanceOf(Binary.class)) //
|
||||
.loadedIsEqualToSource();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void canQueryDeterministicallyEncrypted() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.ssn = "mySecretSSN";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Person loaded = template.query(Person.class).matching(where("ssn").is(source.ssn)).firstValue();
|
||||
assertThat(loaded).isEqualTo(source);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void cannotQueryRandomlyEncrypted() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.wallet = "secret-wallet-id";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Person loaded = template.query(Person.class).matching(where("wallet").is(source.wallet)).firstValue();
|
||||
assertThat(loaded).isNull();
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateSimpleTypeEncryptedFieldWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
template.save(source);
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("ssn", "secret-value"))
|
||||
.first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("ssn")).isInstanceOf(Binary.class)) //
|
||||
.loadedMatches(it -> assertThat(it.getSsn()).isEqualTo("secret-value"));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateComplexTypeEncryptedFieldWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
|
||||
template.save(source);
|
||||
|
||||
Address address = new Address();
|
||||
address.city = "SFO";
|
||||
address.street = "---";
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("address", address)).first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> assertThat(it.get("address")).isInstanceOf(Binary.class)) //
|
||||
.loadedMatches(it -> assertThat(it.getAddress()).isEqualTo(address));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void updateEncryptedFieldInNestedElementWithNewValue() {
|
||||
|
||||
Person source = new Person();
|
||||
source.id = "id-1";
|
||||
source.encryptedZip = new AddressWithEncryptedZip();
|
||||
source.encryptedZip.city = "Boston";
|
||||
source.encryptedZip.street = "central square";
|
||||
|
||||
template.save(source);
|
||||
|
||||
template.update(Person.class).matching(where("id").is(source.id)).apply(Update.update("encryptedZip.zip", "179"))
|
||||
.first();
|
||||
|
||||
verifyThat(source) //
|
||||
.identifiedBy(Person::getId) //
|
||||
.wasSavedMatching(it -> {
|
||||
assertThat(it.get("encryptedZip")).isInstanceOf(Document.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("city")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("street")).isInstanceOf(String.class);
|
||||
assertThat(it.get("encryptedZip", Document.class).get("zip")).isInstanceOf(Binary.class);
|
||||
}) //
|
||||
.loadedMatches(it -> assertThat(it.getEncryptedZip().getZip()).isEqualTo("179"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void aggregationWithMatch() {
|
||||
|
||||
Person person = new Person();
|
||||
person.id = "id-1";
|
||||
person.name = "p1-name";
|
||||
person.ssn = "mySecretSSN";
|
||||
|
||||
template.save(person);
|
||||
|
||||
AggregationResults<Person> aggregationResults = template.aggregateAndReturn(Person.class)
|
||||
.by(newAggregation(Person.class, Aggregation.match(where("ssn").is(person.ssn)))).all();
|
||||
assertThat(aggregationResults.getMappedResults()).containsExactly(person);
|
||||
}
|
||||
|
||||
@Test
|
||||
void altKeyDetection(@Autowired MongoClientEncryption mongoClientEncryption) throws InterruptedException {
|
||||
|
||||
BsonBinary user1key = mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("user-1")));
|
||||
|
||||
BsonBinary user2key = mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("user-2")));
|
||||
|
||||
Person p1 = new Person();
|
||||
p1.id = "id-1";
|
||||
p1.name = "user-1";
|
||||
p1.ssn = "ssn";
|
||||
p1.viaAltKeyNameField = "value-1";
|
||||
|
||||
Person p2 = new Person();
|
||||
p2.id = "id-2";
|
||||
p2.name = "user-2";
|
||||
p2.viaAltKeyNameField = "value-1";
|
||||
|
||||
Person p3 = new Person();
|
||||
p3.id = "id-3";
|
||||
p3.name = "user-1";
|
||||
p3.viaAltKeyNameField = "value-1";
|
||||
|
||||
template.save(p1);
|
||||
template.save(p2);
|
||||
template.save(p3);
|
||||
|
||||
template.execute(Person.class, collection -> {
|
||||
collection.find(new Document()).forEach(it -> System.out.println(it.toJson()));
|
||||
return null;
|
||||
});
|
||||
|
||||
// remove the key and invalidate encrypted data
|
||||
mongoClientEncryption.getClientEncryption().deleteKey(user2key);
|
||||
|
||||
// clear the 60 second key cache within the mongo client
|
||||
mongoClientEncryption.refresh();
|
||||
|
||||
assertThat(template.query(Person.class).matching(where("id").is(p1.id)).firstValue()).isEqualTo(p1);
|
||||
|
||||
assertThatExceptionOfType(PermissionDeniedDataAccessException.class)
|
||||
.isThrownBy(() -> template.query(Person.class).matching(where("id").is(p2.id)).firstValue());
|
||||
}
|
||||
|
||||
<T> SaveAndLoadAssert<T> verifyThat(T source) {
|
||||
return new SaveAndLoadAssert<>(source);
|
||||
}
|
||||
|
||||
class SaveAndLoadAssert<T> {
|
||||
|
||||
T source;
|
||||
Function<T, ?> idProvider;
|
||||
|
||||
SaveAndLoadAssert(T source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> identifiedBy(Function<T, ?> idProvider) {
|
||||
this.idProvider = idProvider;
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> wasSavedAs(Document expected) {
|
||||
return wasSavedMatching(it -> Assertions.assertThat(it).isEqualTo(expected));
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> wasSavedMatching(Consumer<Document> saved) {
|
||||
EncryptionTests.this.assertSaved(source, idProvider, saved);
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedMatches(Consumer<T> expected) {
|
||||
EncryptionTests.this.assertLoaded(source, idProvider, expected);
|
||||
return this;
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedIsEqualToSource() {
|
||||
return loadedIsEqualTo(source);
|
||||
}
|
||||
|
||||
SaveAndLoadAssert<T> loadedIsEqualTo(T expected) {
|
||||
return loadedMatches(it -> Assertions.assertThat(it).isEqualTo(expected));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
<T> void assertSaved(T source, Function<T, ?> idProvider, Consumer<Document> dbValue) {
|
||||
|
||||
Document savedDocument = template.execute(Person.class, collection -> {
|
||||
return collection.find(new Document("_id", idProvider.apply(source))).first();
|
||||
});
|
||||
dbValue.accept(savedDocument);
|
||||
}
|
||||
|
||||
<T> void assertLoaded(T source, Function<T, ?> idProvider, Consumer<T> loadedValue) {
|
||||
|
||||
T loaded = template.query((Class<T>) source.getClass()).matching(where("id").is(idProvider.apply(source)))
|
||||
.firstValue();
|
||||
|
||||
loadedValue.accept(loaded);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class Config extends AbstractMongoClientConfiguration {
|
||||
|
||||
@Autowired ApplicationContext applicationContext;
|
||||
|
||||
@Override
|
||||
protected String getDatabaseName() {
|
||||
return "fle-test";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MongoClient mongoClient() {
|
||||
return super.mongoClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
|
||||
|
||||
converterConfigurationAdapter
|
||||
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext));
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) {
|
||||
|
||||
Lazy<BsonBinary> dataKey = Lazy.of(() -> mongoClientEncryption.getClientEncryption().createDataKey("local",
|
||||
new DataKeyOptions().keyAltNames(Collections.singletonList("mySuperSecretKey"))));
|
||||
|
||||
return new MongoEncryptionConverter(mongoClientEncryption,
|
||||
EncryptionKeyResolver.annotationBased((ctx) -> EncryptionKey.keyId(dataKey.get())));
|
||||
}
|
||||
|
||||
@Bean
|
||||
MongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) {
|
||||
return MongoClientEncryption.caching(() -> ClientEncryptions.create(encryptionSettings));
|
||||
}
|
||||
|
||||
@Bean
|
||||
ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) {
|
||||
|
||||
MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault");
|
||||
MongoCollection<Document> keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName())
|
||||
.getCollection(keyVaultNamespace.getCollectionName());
|
||||
keyVaultCollection.drop();
|
||||
// Ensure that two data keys cannot share the same keyAltName.
|
||||
keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"),
|
||||
new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames")));
|
||||
|
||||
MongoCollection<Document> collection = mongoClient.getDatabase(getDatabaseName()).getCollection("test");
|
||||
collection.drop(); // Clear old data
|
||||
|
||||
final byte[] localMasterKey = new byte[96];
|
||||
new SecureRandom().nextBytes(localMasterKey);
|
||||
Map<String, Map<String, Object>> kmsProviders = new HashMap<String, Map<String, Object>>() {
|
||||
{
|
||||
put("local", new HashMap<String, Object>() {
|
||||
{
|
||||
put("key", localMasterKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Create the ClientEncryption instance
|
||||
ClientEncryptionSettings clientEncryptionSettings = ClientEncryptionSettings.builder()
|
||||
.keyVaultMongoClientSettings(
|
||||
MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build())
|
||||
.keyVaultNamespace(keyVaultNamespace.getFullName()).kmsProviders(kmsProviders).build();
|
||||
return clientEncryptionSettings;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@org.springframework.data.mongodb.core.mapping.Document("test")
|
||||
static class Person {
|
||||
|
||||
String id;
|
||||
String name;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
String ssn;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "mySuperSecretKey") //
|
||||
String wallet;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // full document must be random
|
||||
Address address;
|
||||
|
||||
AddressWithEncryptedZip encryptedZip;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) // lists must be random
|
||||
List<String> listOfString;
|
||||
|
||||
@ExplicitlyEncrypted(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") //
|
||||
String viaAltKeyNameField;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
Map<String, String> mapOfString;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
Map<String, Address> mapOfComplex;
|
||||
}
|
||||
|
||||
@Data
|
||||
static class Address {
|
||||
String city;
|
||||
String street;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
static class AddressWithEncryptedZip extends Address {
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) String zip;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "AddressWithEncryptedZip{" + "zip='" + zip + '\'' + ", city='" + getCity() + '\'' + ", street='"
|
||||
+ getStreet() + '\'' + '}';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.bson.BsonBinary;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import com.mongodb.client.model.vault.EncryptOptions;
|
||||
import com.mongodb.client.vault.ClientEncryption;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MongoClientEncryptionUnitTests {
|
||||
|
||||
@Mock //
|
||||
ClientEncryption clientEncryption;
|
||||
|
||||
@Test // GH-4284
|
||||
void delegatesDecrypt() {
|
||||
|
||||
MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption);
|
||||
mce.decrypt(new BsonBinary(new byte[0]));
|
||||
|
||||
verify(clientEncryption).decrypt(Mockito.any());
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void delegatesEncrypt() {
|
||||
|
||||
MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption);
|
||||
mce.encrypt(new BsonBinary(new byte[0]),
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name")));
|
||||
|
||||
ArgumentCaptor<EncryptOptions> options = ArgumentCaptor.forClass(EncryptOptions.class);
|
||||
verify(clientEncryption).encrypt(any(), options.capture());
|
||||
assertThat(options.getValue().getAlgorithm()).isEqualTo(AEAD_AES_256_CBC_HMAC_SHA_512_Random);
|
||||
assertThat(options.getValue().getKeyAltName()).isEqualTo("sec-key-name");
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void refreshHasNoEffectForFixedClientEncryption() {
|
||||
|
||||
MongoClientEncryption mce = MongoClientEncryption.just(clientEncryption);
|
||||
mce.decrypt(new BsonBinary(new byte[0]));
|
||||
|
||||
assertThat(mce.getClientEncryption()).isSameAs(clientEncryption);
|
||||
assertThat(mce.refresh()).isFalse();
|
||||
assertThat(mce.getClientEncryption()).isSameAs(clientEncryption);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void refreshObtainsNextInstanceFromSupplier() {
|
||||
|
||||
ClientEncryption next = mock(ClientEncryption.class);
|
||||
|
||||
MongoClientEncryption mce = MongoClientEncryption.caching(new Supplier<>() {
|
||||
|
||||
int counter = 0;
|
||||
|
||||
@Override
|
||||
public ClientEncryption get() {
|
||||
return counter++ % 2 == 0 ? clientEncryption : next;
|
||||
}
|
||||
});
|
||||
|
||||
assertThat(mce.getClientEncryption()).isSameAs(clientEncryption);
|
||||
assertThat(mce.refresh()).isTrue();
|
||||
assertThat(mce.getClientEncryption()).isSameAs(next);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
/*
|
||||
* Copyright 2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.encryption;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.bson.BsonArray;
|
||||
import org.bson.BsonBinary;
|
||||
import org.bson.BsonString;
|
||||
import org.bson.BsonValue;
|
||||
import org.bson.Document;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
import org.springframework.data.mongodb.core.convert.MongoConversionContext;
|
||||
import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter;
|
||||
import org.springframework.data.mongodb.core.mapping.ExplicitlyEncrypted;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.test.util.MongoTestMappingContext;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
class MongoEncryptionConverterUnitTests {
|
||||
|
||||
@Mock //
|
||||
Encryption<BsonValue, BsonBinary> encryption;
|
||||
|
||||
@Mock //
|
||||
EncryptionKeyResolver fallbackKeyResolver;
|
||||
|
||||
@Mock //
|
||||
MongoConversionContext conversionContext;
|
||||
|
||||
MongoTestMappingContext mappingContext = MongoTestMappingContext.newTestContext();
|
||||
EncryptionKeyResolver keyResolver;
|
||||
MongoEncryptionConverter converter;
|
||||
|
||||
@Captor ArgumentCaptor<EncryptionOptions> encryptionOptions;
|
||||
|
||||
@Captor ArgumentCaptor<BsonValue> valueToBeEncrypted;
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
|
||||
when(fallbackKeyResolver.getKey(any())).thenReturn(EncryptionKey.altKeyName("default"));
|
||||
when(encryption.encrypt(valueToBeEncrypted.capture(), encryptionOptions.capture()))
|
||||
.thenReturn(new BsonBinary(new byte[0]));
|
||||
keyResolver = EncryptionKeyResolver.annotationBased(fallbackKeyResolver);
|
||||
converter = new MongoEncryptionConverter(encryption, keyResolver);
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void delegatesConversionOfSimpleValueWithDefaultEncryptionKeyFromKeyResolver() {
|
||||
|
||||
when(conversionContext.getProperty())
|
||||
.thenReturn(mappingContext.getPersistentPropertyFor(Type.class, Type::getStringValueWithAlgorithmOnly));
|
||||
|
||||
converter.write("foo", conversionContext);
|
||||
|
||||
assertThat(valueToBeEncrypted.getValue()).isEqualTo(new BsonString("foo"));
|
||||
assertThat(encryptionOptions.getValue()).isEqualTo(
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic).setKey(EncryptionKey.altKeyName("default")));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void favorsAltKeyNameIfPresent() {
|
||||
|
||||
when(conversionContext.getProperty()).thenReturn(
|
||||
mappingContext.getPersistentPropertyFor(Type.class, Type::getStringValueWithAlgorithmAndAltKeyName));
|
||||
|
||||
converter.write("foo", conversionContext);
|
||||
|
||||
assertThat(encryptionOptions.getValue()).isEqualTo(
|
||||
new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("sec-key-name")));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void readsAltKeyNameFromProperty() {
|
||||
|
||||
when(conversionContext.getProperty()).thenReturn(mappingContext.getPersistentPropertyFor(Type.class,
|
||||
Type::getStringValueWithAlgorithmAndAltKeyNameFromPropertyValue));
|
||||
|
||||
ArgumentCaptor<String> path = ArgumentCaptor.forClass(String.class);
|
||||
when(conversionContext.getValue(path.capture())).thenReturn("(ツ)");
|
||||
|
||||
converter.write("foo", conversionContext);
|
||||
assertThat(path.getValue()).isEqualTo("notAnnotated");
|
||||
|
||||
assertThat(encryptionOptions.getValue())
|
||||
.isEqualTo(new EncryptionOptions(AEAD_AES_256_CBC_HMAC_SHA_512_Random).setKey(EncryptionKey.altKeyName("(ツ)")));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void delegatesConversionOfEntityTypes() {
|
||||
|
||||
Document convertedValue = new Document("unencryptedValue", "nested-unencrypted");
|
||||
MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class,
|
||||
Type::getNestedFullyEncrypted);
|
||||
when(conversionContext.getProperty()).thenReturn(property);
|
||||
doReturn(convertedValue).when(conversionContext).write(any(), eq(property.getTypeInformation()));
|
||||
|
||||
ArgumentCaptor<String> path = ArgumentCaptor.forClass(String.class);
|
||||
when(conversionContext.getValue(path.capture())).thenReturn("(ツ)");
|
||||
|
||||
JustATypeWithAnUnencryptedField source = new JustATypeWithAnUnencryptedField();
|
||||
source.unencryptedValue = "nested-unencrypted";
|
||||
|
||||
converter.write(source, conversionContext);
|
||||
|
||||
assertThat(valueToBeEncrypted.getValue()).isEqualTo(convertedValue.toBsonDocument());
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void listsOfSimpleTypesAreConvertedEntirely() {
|
||||
|
||||
MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getListOfString);
|
||||
when(conversionContext.getProperty()).thenReturn(property);
|
||||
|
||||
converter.write(List.of("one", "two"), conversionContext);
|
||||
|
||||
assertThat(valueToBeEncrypted.getValue())
|
||||
.isEqualTo(new BsonArray(List.of(new BsonString("one"), new BsonString("two"))));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void listsOfComplexTypesAreConvertedEntirely() {
|
||||
|
||||
Document convertedValue1 = new Document("unencryptedValue", "nested-unencrypted-1");
|
||||
Document convertedValue2 = new Document("unencryptedValue", "nested-unencrypted-2");
|
||||
|
||||
MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getListOfComplex);
|
||||
when(conversionContext.getProperty()).thenReturn(property);
|
||||
doReturn(convertedValue1, convertedValue2).when(conversionContext).write(any(), eq(property.getTypeInformation()));
|
||||
|
||||
JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField();
|
||||
source1.unencryptedValue = "nested-unencrypted-1";
|
||||
|
||||
JustATypeWithAnUnencryptedField source2 = new JustATypeWithAnUnencryptedField();
|
||||
source2.unencryptedValue = "nested-unencrypted-1";
|
||||
|
||||
converter.write(List.of(source1, source2), conversionContext);
|
||||
|
||||
assertThat(valueToBeEncrypted.getValue())
|
||||
.isEqualTo(new BsonArray(List.of(convertedValue1.toBsonDocument(), convertedValue2.toBsonDocument())));
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void simpleMapsAreConvertedEntirely() {
|
||||
|
||||
MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfString);
|
||||
when(conversionContext.getProperty()).thenReturn(property);
|
||||
doReturn(new Document("k1", "v1").append("k2", "v2")).when(conversionContext).write(any(),
|
||||
eq(property.getTypeInformation()));
|
||||
|
||||
converter.write(Map.of("k1", "v1", "k2", "v2"), conversionContext);
|
||||
|
||||
assertThat(valueToBeEncrypted.getValue())
|
||||
.isEqualTo(new Document("k1", new BsonString("v1")).append("k2", new BsonString("v2")).toBsonDocument());
|
||||
}
|
||||
|
||||
@Test // GH-4284
|
||||
void complexMapsAreConvertedEntirely() {
|
||||
|
||||
Document convertedValue1 = new Document("unencryptedValue", "nested-unencrypted-1");
|
||||
Document convertedValue2 = new Document("unencryptedValue", "nested-unencrypted-2");
|
||||
|
||||
MongoPersistentProperty property = mappingContext.getPersistentPropertyFor(Type.class, Type::getMapOfComplex);
|
||||
when(conversionContext.getProperty()).thenReturn(property);
|
||||
doReturn(new Document("k1", convertedValue1).append("k2", convertedValue2)).when(conversionContext).write(any(),
|
||||
eq(property.getTypeInformation()));
|
||||
|
||||
JustATypeWithAnUnencryptedField source1 = new JustATypeWithAnUnencryptedField();
|
||||
source1.unencryptedValue = "nested-unencrypted-1";
|
||||
|
||||
JustATypeWithAnUnencryptedField source2 = new JustATypeWithAnUnencryptedField();
|
||||
source2.unencryptedValue = "nested-unencrypted-1";
|
||||
|
||||
converter.write(Map.of("k1", source1, "k2", source2), conversionContext);
|
||||
|
||||
assertThat(valueToBeEncrypted.getValue()).isEqualTo(new Document("k1", convertedValue1.toBsonDocument())
|
||||
.append("k2", convertedValue2.toBsonDocument()).toBsonDocument());
|
||||
}
|
||||
|
||||
@Data
|
||||
static class Type {
|
||||
|
||||
String notAnnotated;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
String stringValueWithAlgorithmOnly;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "sec-key-name") //
|
||||
String stringValueWithAlgorithmAndAltKeyName;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random, altKeyName = "/notAnnotated") //
|
||||
String stringValueWithAlgorithmAndAltKeyNameFromPropertyValue;
|
||||
|
||||
@ExplicitlyEncrypted(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) //
|
||||
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
|
||||
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) //
|
||||
Map<String, String> mapOfString;
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Random) //
|
||||
Map<String, JustATypeWithAnUnencryptedField> mapOfComplex;
|
||||
|
||||
RecordWithEncryptedValue recordWithEncryptedValue;
|
||||
|
||||
List<RecordWithEncryptedValue> listOfRecordWithEncryptedValue;
|
||||
}
|
||||
|
||||
static class JustATypeWithAnUnencryptedField {
|
||||
|
||||
String unencryptedValue;
|
||||
}
|
||||
|
||||
static class NestedWithEncryptedField extends JustATypeWithAnUnencryptedField {
|
||||
|
||||
@ExplicitlyEncrypted(algorithm = AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic) //
|
||||
String encryptedValue;
|
||||
}
|
||||
|
||||
record RecordWithEncryptedValue(@ExplicitlyEncrypted String value) {
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,15 @@
|
||||
package org.springframework.data.mongodb.test.util;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.util.MethodInvocationRecorder;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
@@ -45,6 +50,12 @@ public class MongoTestMappingContext extends MongoMappingContext {
|
||||
contextConfig.accept(contextConfigurer);
|
||||
}
|
||||
|
||||
public <T> MongoPersistentProperty getPersistentPropertyFor(Class<T> type, Function<T, ?> property) {
|
||||
|
||||
MongoPersistentEntity<?> persistentEntity = getRequiredPersistentEntity(type);
|
||||
return persistentEntity.getPersistentProperty(MethodInvocationRecorder.forProxyOf(type).record(property).getPropertyPath().get());
|
||||
}
|
||||
|
||||
public MongoTestMappingContext customConversions(MongoConverterConfigurer converterConfig) {
|
||||
|
||||
this.converterConfigurer = converterConfig;
|
||||
@@ -75,4 +86,6 @@ public class MongoTestMappingContext extends MongoMappingContext {
|
||||
public void afterPropertiesSet() {
|
||||
init();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ include::{spring-data-commons-docs}/auditing.adoc[leveloffset=+1]
|
||||
include::reference/mongo-auditing.adoc[leveloffset=+1]
|
||||
include::reference/mapping.adoc[leveloffset=+1]
|
||||
include::reference/sharding.adoc[leveloffset=+1]
|
||||
include::reference/mongo-encryption.adoc[leveloffset=+1]
|
||||
include::reference/kotlin.adoc[leveloffset=+1]
|
||||
include::reference/jmx.adoc[leveloffset=+1]
|
||||
|
||||
|
||||
156
src/main/asciidoc/reference/mongo-encryption.adoc
Normal file
156
src/main/asciidoc/reference/mongo-encryption.adoc
Normal file
@@ -0,0 +1,156 @@
|
||||
[[mongo.encryption]]
|
||||
= Client Side Field Level Encryption (CSFLE)
|
||||
|
||||
Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB.
|
||||
Please make sure to read the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Make sure to set the drivers `com.mongodb.AutoEncryptionSettings` to use client-side encryption.
|
||||
MongoDB does not support encryption for all field types.
|
||||
Specific data types require deterministic encryption to preserve equality comparison functionality.
|
||||
====
|
||||
|
||||
[[mongo.encryption.automatic]]
|
||||
== Automatic Encryption
|
||||
|
||||
MongoDB supports https://www.mongodb.com/docs/manual/core/csfle/[Client-Side Field Level Encryption] out of the box using the MongoDB driver with its Automatic Encryption feature.
|
||||
Automatic Encryption requires a <<mongo.jsonSchema,JSON Schema>> that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step.
|
||||
|
||||
Please refer to the <<mongo.jsonSchema.encrypted-fields,JSON Schema>> section for more information on defining a JSON Schema that holds encryption information.
|
||||
|
||||
To make use of a the `MongoJsonSchema` it needs to be combined with `AutoEncryptionSettings` which can be done eg. via a `MongoClientSettingsBuilderCustomizer`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) {
|
||||
return (builder) -> {
|
||||
|
||||
// ... keyVaultCollection, kmsProvider, ...
|
||||
|
||||
MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext);
|
||||
MongoJsonSchema patientSchema = schemaCreator
|
||||
.filter(MongoJsonSchemaCreator.encryptedOnly())
|
||||
.createSchemaFor(Patient.class);
|
||||
|
||||
AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder()
|
||||
.keyVaultNamespace(keyVaultCollection)
|
||||
.kmsProviders(kmsProviders)
|
||||
.extraOptions(extraOpts)
|
||||
.schemaMap(Collections.singletonMap("db.patient", patientSchema.schemaDocument().toBsonDocument()))
|
||||
.build();
|
||||
|
||||
builder.autoEncryptionSettings(autoEncryptionSettings);
|
||||
};
|
||||
}
|
||||
----
|
||||
|
||||
[[mongo.encryption.explicit]]
|
||||
== Explicit Encryption
|
||||
|
||||
Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform en-/decryption tasks.
|
||||
The `@ExplicitlyEncrypted` annotation is a combination of the `@Encrypted` annotation used for <<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.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
Fields annotated with `@ExplicitlyEncrypted` are always encrypted entirely as outlined in below.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@ExplicitlyEncrypted(...)
|
||||
String simpleValue; <1>
|
||||
|
||||
@ExplicitlyEncrypted(...)
|
||||
Address address; <2>
|
||||
|
||||
@ExplicitlyEncrypted(...)
|
||||
List<...> list; <3>
|
||||
|
||||
@ExplicitlyEncrypted(...)
|
||||
Map<..., ...> mapOfString; <3>
|
||||
----
|
||||
<1> Encrypts the value of the simple type eg. a `String` if not `null`.
|
||||
<2> Encrypts the entire `Address` object and all its nested fields. To only encrypt parts of the `Address`, like `Address#street` the `street` field needs to be annotated.
|
||||
<3> `Collection` like fields are encrypted entirely and not a value by value basis.
|
||||
<4> `Map` like fields are encrypted entirely and not on a key/value basis.
|
||||
====
|
||||
|
||||
Depending on the encryption algorithm MongoDB supports certain operations on an encrypted field using its https://www.mongodb.com/docs/manual/core/queryable-encryption/[Queryable Encryption] feature.
|
||||
To pick a certain algorithm use `@ExplicitlyEncrypted(algorithm = ... )` and choose the required one via `EncryptionAlgorithms`.
|
||||
Please read the https://www.mongodb.com/docs/manual/core/csfle/fundamentals/encryption-algorithms[Encryption Types] manual for more information on algorithms and their usage.
|
||||
|
||||
To perform the actual encryption we do also need a Data Encryption Key (DEK).
|
||||
Please refer to the https://www.mongodb.com/docs/manual/core/csfle/quick-start/#create-a-data-encryption-key[MongoDB Documentation] for more information on how to set up key management and create a Data Encryption Key.
|
||||
The DEK can be referenced directly via its `id` or a defined _alternative name_.
|
||||
The `@EncryptedField` annotation only allows referencing a DEK via an alternative name.
|
||||
It is possible to provide an `EncryptionKeyResolver`, which will be discussed later, to any DEK.
|
||||
|
||||
.Reference the Data Encryption Key
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@EncryptedField(algorithm = ..., altKeyName = "secret-key") <1>
|
||||
String ssn;
|
||||
----
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@EncryptedField(algorithm = ..., altKeyName = "/name") <2>
|
||||
String ssn;
|
||||
----
|
||||
<1> Use the DEK stored with the alternative name `secret-key`.
|
||||
<2> Uses a field reference that will read the actual field value and use that for key lookup. Always requires the full document to be present for save operations. Fields cannot be used in queries/aggregations.
|
||||
====
|
||||
|
||||
By default the `@ExplicitlyEncrypted(value=...)` attribute will reference a `MongoEncryptionConverter`.
|
||||
It is possible to change the default implementation and exchange it with any `PropertyValueConverter` implementation by providing the according type reference.
|
||||
To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the <<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.
|
||||
|
||||
.Sample MongoEncryptionConverter Configuration
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
class Config extends AbstractMongoClientConfiguration {
|
||||
|
||||
// ...
|
||||
|
||||
@Autowired ApplicationContext appContext;
|
||||
|
||||
@Bean
|
||||
MongoEncryptionConverter encryptingConverter() {
|
||||
|
||||
ClientEncryptionSettings encryptionSettings = ClientEncryptionSettings.builder()
|
||||
// ...
|
||||
|
||||
Encryption<BsonValue, BsonBinary> encryption = MongoClientEncryption.just(ClientEncryptions.create(encryptionSettings)) <1>
|
||||
EncryptionKeyResolver keyResolver = EncryptionKeyResolver.annotationBased((ctx) -> ...); <2>
|
||||
|
||||
return new MongoEncryptionConverter(encryption, keyResolver); <3>
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureConverters(MongoConverterConfigurationAdapter adapter) {
|
||||
|
||||
adapter
|
||||
.registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(appContext)); <4>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Set up a `com.mongodb.client.vault.ClientEncryption` specific `Encryption` engine.
|
||||
<2> Read the `EncryptionKey` from annotations on the field.
|
||||
<3> Create the `MongoEncryptionConverter`.
|
||||
<4> Enable for a `PropertyValueConverter` within the `BeanFactory`.
|
||||
====
|
||||
@@ -396,37 +396,8 @@ public class EncryptionExtension implements EvaluationContextExtension {
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
To combine derived encryption settings with `AutoEncryptionSettings` in a Spring Boot application use the `MongoClientSettingsBuilderCustomizer`.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
@Bean
|
||||
MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) {
|
||||
return (builder) -> {
|
||||
|
||||
// ... keyVaultCollection, kmsProvider, ...
|
||||
|
||||
MongoJsonSchemaCreator schemaCreator = MongoJsonSchemaCreator.create(mappingContext);
|
||||
MongoJsonSchema patientSchema = schemaCreator
|
||||
.filter(MongoJsonSchemaCreator.encryptedOnly())
|
||||
.createSchemaFor(Patient.class);
|
||||
|
||||
AutoEncryptionSettings autoEncryptionSettings = AutoEncryptionSettings.builder()
|
||||
.keyVaultNamespace(keyVaultCollection)
|
||||
.kmsProviders(kmsProviders)
|
||||
.extraOptions(extraOpts)
|
||||
.schemaMap(Collections.singletonMap("db.patient", patientSchema.schemaDocument().toBsonDocument()))
|
||||
.build();
|
||||
|
||||
builder.autoEncryptionSettings(autoEncryptionSettings);
|
||||
};
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
NOTE: Make sure to set the drivers `com.mongodb.AutoEncryptionSettings` to use client-side encryption. MongoDB does not support encryption for all field types. Specific data types require deterministic encryption to preserve equality comparison functionality.
|
||||
|
||||
[[mongo.jsonSchema.types]]
|
||||
==== JSON Schema Types
|
||||
|
||||
|
||||
Reference in New Issue
Block a user