Field Level Encryption Support. (#1546)

Note that an @Encrypted property object that contains an @Encrypted property is
not supported and will result in an exception.

Closes #763.
This commit is contained in:
Michael Reiche
2022-08-29 10:45:57 -07:00
committed by GitHub
parent 712d7d244c
commit 921b8c2e64
16 changed files with 1005 additions and 64 deletions

View File

@@ -200,6 +200,13 @@ The generated documentation is available from `target/site/reference/html/index.
popd
----
=== Intellij Issue with Importing pom.xml
There is an issue in Intellij that prevents it from importing modules when one of the module
directories has the same name as the project directory. The work-around is to create a new module (any name, any type will suffice).
When Intellij creates the new module, it will also recognize the existing modules. Once the new module is
created, it can be deleted and Intellij will now recognize the existing modules.
The generated documentation is available from `target/site/reference/html/index.html`.
== Examples

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
@@ -217,6 +218,13 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.couchbase.client</groupId>
<artifactId>couchbase-encryption</artifactId>
<version>3.1.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
@@ -303,7 +311,9 @@
</goals>
<configuration>
<outputDirectory>target/generated-test-sources</outputDirectory>
<processor>org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor</processor>
<processor>
org.springframework.data.couchbase.repository.support.CouchbaseAnnotationProcessor
</processor>
</configuration>
</execution>
</executions>

View File

@@ -18,8 +18,9 @@ package org.springframework.data.couchbase.config;
import static com.couchbase.client.java.ClusterOptions.clusterOptions;
import java.util.Collections;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -27,6 +28,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.couchbase.CouchbaseClientFactory;
@@ -35,6 +37,7 @@ import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
import org.springframework.data.couchbase.core.convert.CouchbaseCustomConversions;
import org.springframework.data.couchbase.core.convert.MappingCouchbaseConverter;
import org.springframework.data.couchbase.core.convert.OtherConverters;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
@@ -149,7 +152,9 @@ public abstract class AbstractCouchbaseConfiguration {
if (!nonShadowedJacksonPresent()) {
throw new CouchbaseException("non-shadowed Jackson not present");
}
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper()));
CryptoManager cryptoManager = cryptoManager();
builder.jsonSerializer(JacksonJsonSerializer.create(couchbaseObjectMapper(cryptoManager)));
builder.cryptoManager(cryptoManager);
configureEnvironment(builder);
return builder.build();
}
@@ -280,8 +285,8 @@ public abstract class AbstractCouchbaseConfiguration {
@Bean
public TranslationService couchbaseTranslationService() {
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
jacksonTranslationService.setObjectMapper(couchbaseObjectMapper(cryptoManager()));
jacksonTranslationService.afterPropertiesSet();
// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...
JacksonTransformers.MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return jacksonTranslationService;
@@ -308,10 +313,25 @@ public abstract class AbstractCouchbaseConfiguration {
*/
public ObjectMapper couchbaseObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
return couchbaseObjectMapper(cryptoManager());
}
/**
* Creates a {@link ObjectMapper} for the jsonSerializer of the ClusterEnvironment
*
* @param cryptoManager
* @return ObjectMapper
*/
ObjectMapper mapper;
public ObjectMapper couchbaseObjectMapper(CryptoManager cryptoManager) {
if (mapper != null) {
return mapper;
}
mapper = new ObjectMapper(); // or use the one from the Java SDK (?) JacksonTransformers.MAPPER
mapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JsonValueModule());
CryptoManager cryptoManager = null;
if (cryptoManager != null) {
mapper.registerModule(new EncryptionModule(cryptoManager));
}
@@ -320,7 +340,7 @@ public abstract class AbstractCouchbaseConfiguration {
/**
* The default blocking transaction manager. It is an implementation of CallbackPreferringTransactionManager
* CallbackPreferrringTransactionmanagers do not play well with test-cases that rely
* CallbackPreferringTransactionManagers do not play well with test-cases that rely
* on @TestTransaction/@BeforeTransaction/@AfterTransaction
*
* @param clientFactory
@@ -341,6 +361,7 @@ public abstract class AbstractCouchbaseConfiguration {
TransactionTemplate couchbaseTransactionTemplate(CouchbaseCallbackTransactionManager couchbaseTransactionManager) {
return new TransactionTemplate(couchbaseTransactionManager);
}
/**
* The default TransactionalOperator.
*
@@ -379,11 +400,28 @@ public abstract class AbstractCouchbaseConfiguration {
* and {@link #couchbaseMappingContext(CustomConversions)}. Returns an empty {@link CustomConversions} instance by
* default.
*
* @param cryptoManagerOptional optional cryptoManager. Make varargs for backwards compatibility.
* @return must not be {@literal null}.
*/
@Bean(name = BeanNames.COUCHBASE_CUSTOM_CONVERSIONS)
public CustomConversions customConversions() {
return new CouchbaseCustomConversions(Collections.emptyList());
public CustomConversions customConversions(CryptoManager... cryptoManagerOptional) {
assert (cryptoManagerOptional == null || cryptoManagerOptional.length <= 1);
CryptoManager cryptoManager = cryptoManagerOptional != null && cryptoManagerOptional.length == 1
? cryptoManagerOptional[0]
: null;
List<GenericConverter> newConverters = new ArrayList();
// the cryptoConverters take an argument, so they cannot be created in the
// static block of CouchbaseCustomConversions. And they must be set before the super() constructor
// in CouchbaseCustomerConversions
if (cryptoManager != null) {
newConverters.addAll(OtherConverters.getCryptoConverters(cryptoManager));
}
return new CouchbaseCustomConversions(newConverters);
}
@Bean
protected CryptoManager cryptoManager() {
return null;
}
/**

View File

@@ -20,8 +20,11 @@ import java.util.Collections;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.mapping.model.EntityInstantiators;
/**
@@ -29,6 +32,7 @@ import org.springframework.data.mapping.model.EntityInstantiators;
*
* @author Michael Nitschinger
* @author Mark Paluch
* @author Michael Reiche
*/
public abstract class AbstractCouchbaseConverter implements CouchbaseConverter, InitializingBean {
@@ -93,6 +97,36 @@ public abstract class AbstractCouchbaseConverter implements CouchbaseConverter,
conversions.registerConvertersIn(conversionService);
}
/**
* This convertForWriteIfNeeded takes a property and accessor so that the annotations can be accessed (ie. @Encrypted)
*
* @param prop the property to be converted to the class that would actually be stored.
* @param accessor the property accessor
* @return
*/
@Override
public Object convertForWriteIfNeeded(CouchbasePersistentProperty prop, ConvertingPropertyAccessor<Object> accessor) {
Object value = accessor.getProperty(prop, prop.getType());
if (value == null) {
return null;
}
Object result = this.conversions.getCustomWriteTarget(prop.getType()) //
.map(it -> this.conversionService.convert(value, new TypeDescriptor(prop.getField()),
TypeDescriptor.valueOf(it))) //
.orElseGet(() -> Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value);
return result;
}
/**
* This convertForWriteIfNeed takes only the value to convert. It cannot access the annotations of the Field being
* converted.
*
* @param value the value to be converted to the class that would actually be stored.
* @return
*/
@Override
public Object convertForWriteIfNeeded(Object value) {
if (value == null) {

View File

@@ -22,6 +22,7 @@ import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
import org.springframework.data.util.TypeInformation;
/**
@@ -37,13 +38,24 @@ public interface CouchbaseConverter
/**
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
* needed.
* needed. This method cannot access the annotations of the field.
*
* @param value the value to be converted to the class that would actually be stored.
* @return the converted value (or the same value if no conversion necessary).
*/
Object convertForWriteIfNeeded(Object value);
/**
* Convert the value if necessary to the class that would actually be stored, or leave it as is if no conversion
* needed. This method can access the annotations of the field.
*
* @param source the property to be converted to the class that would actually be stored.
* @param accessor the property accessor
* @return the converted value (or the same value if no conversion necessary).
*/
Object convertForWriteIfNeeded(final CouchbasePersistentProperty source,
final ConvertingPropertyAccessor<Object> accessor);
/**
* Return the Class that would actually be stored for a given Class.
*

View File

@@ -22,6 +22,8 @@ import java.util.List;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import com.couchbase.client.core.encryption.CryptoManager;
/**
* Value object to capture custom conversion.
* <p>
@@ -32,6 +34,7 @@ import org.springframework.data.mapping.model.SimpleTypeHolder;
* @author Oliver Gierke
* @author Mark Paluch
* @author Subhashni Balakrishnan
* @Michael Reiche
* @see org.springframework.data.convert.CustomConversions
* @see SimpleTypeHolder
* @since 2.0
@@ -42,6 +45,18 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert
private static final List<Object> STORE_CONVERTERS;
private CryptoManager cryptoManager;
/**
* Expose the CryptoManager used by a DecryptingReadingConverter or EncryptingWritingConverter, if any. There can only
* be one. MappingCouchbaseConverter needs it.
*
* @return cryptoManager
*/
public CryptoManager getCryptoManager() {
return cryptoManager;
}
static {
List<Object> converters = new ArrayList<>();
@@ -61,5 +76,31 @@ public class CouchbaseCustomConversions extends org.springframework.data.convert
*/
public CouchbaseCustomConversions(final List<?> converters) {
super(STORE_CONVERSIONS, converters);
for (Object c : converters) {
if (c instanceof DecryptingReadingConverter) {
CryptoManager foundCryptoManager = ((DecryptingReadingConverter) c).cryptoManager;
if (foundCryptoManager == null) {
throw new RuntimeException(("DecryptingReadingConverter must have a cryptoManager"));
} else {
if (cryptoManager != null && this.cryptoManager != cryptoManager) {
throw new RuntimeException(
"all DecryptingReadingConverters and EncryptingWringConverters must use " + " a single CryptoManager");
}
}
cryptoManager = foundCryptoManager;
}
if (c instanceof EncryptingWritingConverter) {
CryptoManager foundCryptoManager = ((EncryptingWritingConverter) c).cryptoManager;
if (foundCryptoManager == null) {
throw new RuntimeException(("EncryptingWritingConverter must have a cryptoManager"));
} else {
if (cryptoManager != null && this.cryptoManager != cryptoManager) {
throw new RuntimeException(
"all DecryptingReadingConverters and EncryptingWringConverters must use " + " a single CryptoManager");
}
}
cryptoManager = foundCryptoManager;
}
}
}
}

View File

@@ -0,0 +1,71 @@
/*
* Copyright 2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.Set;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.java.encryption.annotation.Encrypted;
/**
* Use the cryptoManager to decrypt a field
*
* @author Michael Reiche
*/
@ReadingConverter
public class DecryptingReadingConverter implements ConditionalGenericConverter {
CryptoManager cryptoManager;
ConversionService conversionService;
public DecryptingReadingConverter(CryptoManager cryptoManager) {
this.cryptoManager = cryptoManager;
}
public void setConversionService(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> convertiblePairs = new HashSet<>();
Class<?>[] clazzes = new Class[] { String.class, Integer.class, Long.class, Float.class, Double.class,
BigInteger.class, BigDecimal.class, Boolean.class };
for (Class clazz : clazzes) {
convertiblePairs.add(new ConvertiblePair(CouchbaseDocument.class, clazz));
}
return convertiblePairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return source == null? null : new String(cryptoManager.decrypt(((CouchbaseDocument) source).getContent()));
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return targetType.hasAnnotation(Encrypted.class);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import java.util.Map;
/**
*
* @author Michael Reiche
*/
public class Encrypted {
Map<Object,Object> encryptionMap;
public Encrypted(Map<Object,Object> encryptionMap){
this.encryptionMap = encryptionMap;
}
Map<Object, Object> getEncryptionMap(){
return encryptionMap;
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.core.convert;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalGenericConverter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.convert.WritingConverter;
import com.couchbase.client.core.encryption.CryptoManager;
/**
* Use the cryptomManager to encrypt properties
*
* @author Michael Reiche
*/
@WritingConverter
public class EncryptingWritingConverter implements ConditionalGenericConverter {
CryptoManager cryptoManager;
public EncryptingWritingConverter(CryptoManager cryptoManager) {
this.cryptoManager = cryptoManager;
}
@Override
public Set<GenericConverter.ConvertiblePair> getConvertibleTypes() {
Set<ConvertiblePair> convertiblePairs = new HashSet<>();
Class<?>[] clazzes = new Class[] { String.class, Integer.class, Long.class, Float.class, Double.class,
BigInteger.class, BigDecimal.class, Boolean.class };
for (Class clazz : clazzes) {
convertiblePairs.add(new ConvertiblePair(clazz, String.class));
}
return convertiblePairs;
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
if (source == null) {
return null;
}
com.couchbase.client.java.encryption.annotation.Encrypted ann = sourceType
.getAnnotation(com.couchbase.client.java.encryption.annotation.Encrypted.class);
Map<Object, Object> result = new HashMap<>();
result.putAll(cryptoManager.encrypt(source.toString().getBytes(), ann.encrypter()));
return new Encrypted(result);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return sourceType.hasAnnotation(com.couchbase.client.java.encryption.annotation.Encrypted.class);
}
}

View File

@@ -19,9 +19,11 @@ package org.springframework.data.couchbase.core.convert;
import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.UNIQUE;
import static org.springframework.data.couchbase.core.mapping.id.GenerationStrategy.USE_ATTRIBUTES;
import java.beans.Transient;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -33,9 +35,11 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.couchbase.core.convert.translation.JacksonTranslationService;
import org.springframework.data.couchbase.core.convert.translation.TranslationService;
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
import org.springframework.data.couchbase.core.mapping.CouchbaseList;
import org.springframework.data.couchbase.core.mapping.CouchbaseMappingContext;
@@ -72,6 +76,9 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.java.json.JsonObject;
/**
* A mapping converter for Couchbase. The converter is responsible for reading from and writing to entities and
* converting it into a consumable database representation.
@@ -119,6 +126,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*/
private @Nullable EntityCallbacks entityCallbacks;
/**
* CryptoManager for en/decryption
*/
private @Nullable CryptoManager cryptoManager;
public MappingCouchbaseConverter() {
this(new CouchbaseMappingContext(), null);
}
@@ -156,6 +168,23 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
spELContext = new SpELContext(CouchbaseDocumentPropertyAccessor.INSTANCE);
}
/**
* Get the cryptoManager used in conversions. We need to be able to mangle a property name.
*/
@Override
public void setCustomConversions(CustomConversions customConversions) {
super.setCustomConversions(customConversions);
if (customConversions instanceof CouchbaseCustomConversions) {
this.cryptoManager = ((CouchbaseCustomConversions) customConversions).getCryptoManager();
}
}
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
}
/**
* Returns a collection from the given source object.
*
@@ -278,8 +307,15 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
accessor.setProperty(prop, obj);
}
/**
* doesPropertyExistInSource. It could have getFieldName() or mangled(getFieldName())
*
* @param property
* @return
*/
private boolean doesPropertyExistInSource(final CouchbasePersistentProperty property) {
return property.isIdProperty() || source.containsKey(property.getFieldName());
return property.isIdProperty() || source.containsKey(property.getFieldName())
|| (cryptoManager != null && source.containsKey(cryptoManager.mangle(property.getFieldName())));
}
private boolean isIdConstructionProperty(final CouchbasePersistentProperty property) {
@@ -398,10 +434,25 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
throw new MappingException("Unable to create class from " + value.toString());
}
}
return target.isAssignableFrom(value.getClass()) ? value : conversionService.convert(value, target);
}
/**
* Potentially convert simple values like ENUMs.
*
* @param value the value to convert.
* @param target the target persistent property which may have an Encrypt annotation
* @return the potentially converted object.
*/
@SuppressWarnings("unchecked")
private Object getPotentiallyConvertedSimpleRead(Object value, final CouchbasePersistentProperty target) {
if (value == null || target == null) {
return value;
}
// this call to convert takes TypeDescriptors - the target type descriptor may have an Encrypt annotation.
return conversionService.convert(value, TypeDescriptor.forObject(value), new TypeDescriptor(target.getField()));
}
@Override
public void write(final Object source, final CouchbaseDocument target) {
if (source == null) {
@@ -415,7 +466,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
typeMapper.writeType(type, target);
}
writeInternal(source, target, type, true);
// CouchbasePersistentEntity entity = mappingContext.getPersistentEntity(source.getClass());
writeInternalRoot(source, target, type, true);
if (target.getId() == null) {
throw new MappingException("An ID property is needed, but not found/could not be generated on this entity.");
}
@@ -426,10 +478,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*
* @param source the source object.
* @param target the target document.
* @param typeHint the type information for the source.
* @param withId write out with the id.
*/
@SuppressWarnings("unchecked")
protected void writeInternal(final Object source, CouchbaseDocument target, final TypeInformation<?> typeHint,
protected void writeInternalRoot(final Object source, CouchbaseDocument target, TypeInformation<?> typeHint,
boolean withId) {
if (source == null) {
return;
@@ -442,7 +494,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
}
if (Map.class.isAssignableFrom(source.getClass())) {
writeMapInternal((Map<Object, Object>) source, target, ClassTypeInformation.MAP);
writeMapInternal((Map<Object, Object>) source, target, ClassTypeInformation.MAP, null);
return;
}
@@ -451,7 +503,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
}
CouchbasePersistentEntity<?> entity = mappingContext.getPersistentEntity(source.getClass());
writeInternal(source, target, entity, withId);
writeInternalEntity(source, target, entity, withId, null);
addCustomTypeKeyIfNecessary(typeHint, source, target);
}
@@ -487,8 +539,8 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
* @param entity the persistent entity to convert from.
* @param withId one of the top-level properties is the id for the document
*/
protected void writeInternal(final Object source, final CouchbaseDocument target,
final CouchbasePersistentEntity<?> entity, boolean withId) {
protected void writeInternalEntity(final Object source, final CouchbaseDocument target,
final CouchbasePersistentEntity<?> entity, boolean withId, CouchbasePersistentProperty prop) {
if (source == null) {
return;
}
@@ -508,6 +560,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
target.setExpiration((int) (entity.getExpiryDuration().getSeconds()));
// write all the entity.properties to the target. Does not write the id or version.
writeToTargetDocument(target, entity, accessor, idProperty, versionProperty, prefixes, suffixes, idAttributes);
if (idProperty != null && target.getId() == null) {
@@ -521,6 +574,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
} else {
target.setId(id);
}
}
entity.doWithAssociations(new AssociationHandler<CouchbasePersistentProperty>() {
@@ -530,7 +584,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
Class<?> type = inverseProp.getType();
Object propertyObj = accessor.getProperty(inverseProp, type);
if (null != propertyObj) {
writePropertyInternal(propertyObj, target, inverseProp, false);
writePropertyInternal(propertyObj, target, inverseProp, accessor);
}
}
});
@@ -577,9 +631,9 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
}
if (!conversions.isSimpleType(propertyObj.getClass())) {
writePropertyInternal(propertyObj, target, prop, false);
writePropertyInternal(propertyObj, target, prop, accessor);
} else {
writeSimpleInternal(propertyObj, target, prop.getFieldName());
writeSimpleInternal(prop, accessor, target, prop.getFieldName());
}
}
}
@@ -595,7 +649,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*/
@SuppressWarnings("unchecked")
private void writePropertyInternal(final Object source, final CouchbaseDocument target,
final CouchbasePersistentProperty prop, boolean withId) {
final CouchbasePersistentProperty prop, final ConvertingPropertyAccessor accessor) {
if (source == null) {
return;
}
@@ -603,28 +657,23 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
String name = prop.getFieldName();
TypeInformation<?> valueType = ClassTypeInformation.from(source.getClass());
TypeInformation<?> type = prop.getTypeInformation();
if (valueType.isCollectionLike()) {
CouchbaseList collectionDoc = createCollection(asCollection(source), prop);
CouchbaseList collectionDoc = createCollection(asCollection(source), valueType, prop, accessor);
// TODO needs to handle enc
target.put(name, collectionDoc);
return;
}
if (valueType.isMap()) {
CouchbaseDocument mapDoc = createMap((Map<Object, Object>) source, prop);
// TODO needs to handle enc
target.put(name, mapDoc);
return;
}
if (valueType.getType().equals(java.util.Optional.class)) {
if (source == null)
return;
Optional<String> o = (Optional<String>) source;
if (o.isPresent()) {
writeSimpleInternal(o.get(), target, prop.getFieldName());
} else {
writeSimpleInternal(null, target, prop.getFieldName());
}
Optional<?> o = (Optional<?>) source;
writeSimpleInternal(o.map(s -> prop).orElse(null), accessor, target, prop.getFieldName());
return;
}
@@ -643,9 +692,19 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
CouchbasePersistentEntity<?> entity = isSubtype(prop.getType(), source.getClass())
? mappingContext.getRequiredPersistentEntity(source.getClass())
: mappingContext.getRequiredPersistentEntity(type);
writeInternal(source, propertyDoc, entity, false);
target.put(name, propertyDoc);
: mappingContext.getRequiredPersistentEntity(prop);
writeInternalEntity(source, propertyDoc, entity, false, prop);
com.couchbase.client.java.encryption.annotation.Encrypted ann = prop
.findAnnotation(com.couchbase.client.java.encryption.annotation.Encrypted.class);
if (ann != null) {
JsonObject jo = JsonObject.from(propertyDoc.getContent());
Map<Object, Object> encMap = new HashMap();
encMap.putAll(cryptoManager.encrypt(jo.toBytes(), ann.encrypter()));
CouchbaseDocument mapDoc = writeMapInternal(encMap, new CouchbaseDocument(), prop.getTypeInformation(), prop);
target.put(cryptoManager.mangle(name), mapDoc);
} else {
target.put(name, propertyDoc);
}
}
/**
@@ -659,7 +718,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
Assert.notNull(map, "Given map must not be null!");
Assert.notNull(prop, "PersistentProperty must not be null!");
return writeMapInternal(map, new CouchbaseDocument(), prop.getTypeInformation());
return writeMapInternal(map, new CouchbaseDocument(), prop.getTypeInformation(), prop);
}
/**
@@ -667,11 +726,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*
* @param source the source object.
* @param target the target document.
* @param type the type information for the document.
* @return the written couchbase document.
*/
private CouchbaseDocument writeMapInternal(final Map<Object, Object> source, final CouchbaseDocument target,
final TypeInformation<?> type) {
TypeInformation<?> type, CouchbasePersistentProperty prop) {
for (Map.Entry<Object, Object> entry : source.entrySet()) {
Object key = entry.getKey();
Object val = entry.getValue();
@@ -680,14 +738,14 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
String simpleKey = key.toString();
if (val == null || conversions.isSimpleType(val.getClass())) {
writeSimpleInternal(val, target, simpleKey);
writeSimpleInternal(val, target, simpleKey); // this is an entry in a map, cannot have an annotation
} else if (val instanceof Collection || val.getClass().isArray()) {
target.put(simpleKey, writeCollectionInternal(asCollection(val),
new CouchbaseList(conversions.getSimpleTypeHolder()), type.getMapValueType()));
target.put(simpleKey,
writeCollectionInternal(asCollection(val), new CouchbaseList(conversions.getSimpleTypeHolder()),
prop.getTypeInformation(), prop, getPropertyAccessor(val)));
} else {
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
TypeInformation<?> valueTypeInfo = type.isMap() ? type.getMapValueType() : ClassTypeInformation.OBJECT;
writeInternal(val, embeddedDoc, valueTypeInfo, false);
writeInternalRoot(val, embeddedDoc, prop.getTypeInformation(), false);
target.put(simpleKey, embeddedDoc);
}
} else {
@@ -702,12 +760,12 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
* Helper method to create the underlying collection/list.
*
* @param collection the collection to write.
* @param prop the property information.
* @return the created couchbase list.
*/
private CouchbaseList createCollection(final Collection<?> collection, final CouchbasePersistentProperty prop) {
return writeCollectionInternal(collection, new CouchbaseList(conversions.getSimpleTypeHolder()),
prop.getTypeInformation());
private CouchbaseList createCollection(final Collection<?> collection, final TypeInformation<?> type,
CouchbasePersistentProperty prop, ConvertingPropertyAccessor accessor) {
return writeCollectionInternal(collection, new CouchbaseList(conversions.getSimpleTypeHolder()), type, prop,
accessor);
}
/**
@@ -715,11 +773,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
*
* @param source the source object.
* @param target the target document.
* @param type the type information for the document.
* @return the created couchbase list.
*/
private CouchbaseList writeCollectionInternal(final Collection<?> source, final CouchbaseList target,
final TypeInformation<?> type) {
final TypeInformation<?> type, CouchbasePersistentProperty prop, ConvertingPropertyAccessor accessor) {
TypeInformation<?> componentType = type == null ? null : type.getComponentType();
for (Object element : source) {
@@ -729,11 +787,10 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
target.put(getPotentiallyConvertedSimpleWrite(element));
} else if (element instanceof Collection || elementType.isArray()) {
target.put(writeCollectionInternal(asCollection(element), new CouchbaseList(conversions.getSimpleTypeHolder()),
componentType));
type, prop, accessor));
} else {
CouchbaseDocument embeddedDoc = new CouchbaseDocument();
writeInternal(element, embeddedDoc, componentType, false);
writeInternalRoot(element, embeddedDoc, prop.getTypeInformation(), false);
target.put(embeddedDoc);
}
@@ -784,7 +841,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
/**
* Write the given source into the couchbase document target.
*
* @param source the source object.
* @param source the source object. This does not have access to annotaions.
* @param target the target document.
* @param key the key of the object.
*/
@@ -792,8 +849,41 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
target.put(key, getPotentiallyConvertedSimpleWrite(source));
}
/**
* Write the given source into the couchbase document target.
*
* @param source the source persistent property. This has access to annotations.
* @param target the target document.
* @param key the key of the object.
*/
private void writeSimpleInternal(final CouchbasePersistentProperty source,
final ConvertingPropertyAccessor<Object> accessor, final CouchbaseDocument target, final String key) {
Object result = getPotentiallyConvertedSimpleWrite(source, accessor);
if (result instanceof Optional) {
Optional<Object> optional = (Optional) result;
result = optional.orElse(null);
}
// For FLE, write as "encrypted$"...
if (source.findAnnotation(com.couchbase.client.java.encryption.annotation.Encrypted.class) != null) {
if (result instanceof Encrypted) {
CouchbaseDocument mapDoc = writeMapInternal(((Encrypted) result).getEncryptionMap(), new CouchbaseDocument(),
ClassTypeInformation.MAP, null);
target.put(cryptoManager.mangle(key), mapDoc);
} else {
throw new RuntimeException("field annotation with @Encrypted could not be encrypted " + source.getField());
}
} else {
target.put(key, result);
}
}
public Object getPotentiallyConvertedSimpleWrite(final Object value) {
return convertForWriteIfNeeded(value);
return convertForWriteIfNeeded(value); // cannot access annotations
}
public Object getPotentiallyConvertedSimpleWrite(final CouchbasePersistentProperty value,
ConvertingPropertyAccessor<Object> accessor) {
return convertForWriteIfNeeded(value, accessor); // can access annotations
}
/**
@@ -845,7 +935,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
* @return the converted object.
*/
@SuppressWarnings("unchecked")
private <R> R readValue(Object value, TypeInformation<?> type, Object parent) {
private <R> R readValue(Object value, TypeInformation type, Object parent) {
Class<?> rawType = type.getType();
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
@@ -855,7 +945,43 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
} else if (value instanceof CouchbaseList) {
return (R) readCollection(type, (CouchbaseList) value, parent);
} else {
return (R) getPotentiallyConvertedSimpleRead(value, rawType);
return (R) getPotentiallyConvertedSimpleRead(value, type.getClass()); // type does not have annotations
}
}
TranslationService translationService = new JacksonTranslationService();
/**
* Helper method to read the value based on the PersistentProperty
*
* @param value the value to convert.
* @param prop the persistent property - will have annotations (i.e. Encrypt for FLE)
* @param parent the optional parent.
* @param <R> the target type.
* @return the converted object.
*/
@SuppressWarnings("unchecked")
private <R> R readValue(Object value, CouchbasePersistentProperty prop, Object parent) {
Class<?> rawType = prop.getType();
if (prop.findAnnotation(com.couchbase.client.java.encryption.annotation.Encrypted.class) != null
&& !conversions.isSimpleType(rawType)) {
TreeMap exported = ((CouchbaseDocument) value).export(); // should be couchbase document (?)
byte[] result = cryptoManager.decrypt(exported);
CouchbaseDocument converted = new CouchbaseDocument();
Object readEntity = read(prop.getTypeInformation().getType(),
(CouchbaseDocument) translationService.decode(new String(result), converted));
return (R) readEntity;
}
if (conversions.hasCustomReadTarget(value.getClass(), rawType)) {
TypeInformation ti = ClassTypeInformation.from(value.getClass());
return (R) conversionService.convert(value, ti.toTypeDescriptor(), new TypeDescriptor(prop.getField()));
} else if (value instanceof CouchbaseDocument) {
return (R) read(prop.getTypeInformation(), (CouchbaseDocument) value, parent);
} else if (value instanceof CouchbaseList) {
return (R) readCollection(prop.getTypeInformation(), (CouchbaseList) value, parent);
} else {
return (R) getPotentiallyConvertedSimpleRead(value, prop);// passes PersistentProperty with annotations
}
}
@@ -961,6 +1087,11 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
public <R> R getPropertyValue(final CouchbasePersistentProperty property) {
String expression = property.getSpelExpression();
Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName());
if (value == null
&& property.findAnnotation(com.couchbase.client.java.encryption.annotation.Encrypted.class) != null
&& cryptoManager != null) {
value = source.get(cryptoManager.mangle(property.getFieldName()));
}
if (property == entity.getIdProperty() && parent == null) {
return readValue(source.getId(), property.getTypeInformation(), source);
@@ -968,8 +1099,7 @@ public class MappingCouchbaseConverter extends AbstractCouchbaseConverter implem
if (value == null) {
return null;
}
return readValue(value, property.getTypeInformation(), source);
return readValue(value, property, source);
}
}

View File

@@ -23,7 +23,9 @@ import java.util.Collection;
import java.util.List;
import java.util.UUID;
import com.couchbase.client.core.encryption.CryptoManager;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
@@ -43,7 +45,6 @@ public final class OtherConverters {
*/
public static Collection<Converter<?, ?>> getConvertersToRegister() {
List<Converter<?, ?>> converters = new ArrayList<Converter<?, ?>>();
converters.add(UuidToString.INSTANCE);
converters.add(StringToUuid.INSTANCE);
converters.add(BigIntegerToString.INSTANCE);
@@ -54,6 +55,19 @@ public final class OtherConverters {
return converters;
}
/**
* Returns the crypto converters to be registered.
*
* @param cryptoManager to use for encrypting and decrypting
* @return the list of converters to register.
*/
public static Collection<GenericConverter> getCryptoConverters(CryptoManager cryptoManager) {
List<GenericConverter> converters = new ArrayList<>();
converters.add(new EncryptingWritingConverter(cryptoManager));
converters.add(new DecryptingReadingConverter(cryptoManager));
return converters;
}
@WritingConverter
public enum UuidToString implements Converter<UUID, String> {
INSTANCE;

View File

@@ -15,10 +15,16 @@
*/
package org.springframework.data.couchbase.domain;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import org.springframework.data.couchbase.core.mapping.Document;
import java.io.Serializable;
@Document
public class Address extends ComparableEntity {
public class Address extends ComparableEntity implements Serializable {
@Encrypted
private String encStreet;
private String street;
private String city;
@@ -36,6 +42,14 @@ public class Address extends ComparableEntity {
this.street = street;
}
public String getEncStreet() {
return encStreet;
}
public void setEncStreet(String encStreet) {
this.encStreet = encStreet;
}
public String getCity() {
return city;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -220,6 +220,7 @@ public class Config extends AbstractCouchbaseConfiguration {
@Bean(name = "couchbaseTranslationService")
public TranslationService couchbaseTranslationService() {
final JacksonTranslationService jacksonTranslationService = new JacksonTranslationService();
jacksonTranslationService.setObjectMapper(couchbaseObjectMapper(cryptoManager()));
jacksonTranslationService.afterPropertiesSet();
// for sdk3, we need to ask the mapper _it_ uses to ignore extra fields...

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.annotation.Transient;
import org.springframework.data.annotation.TypeAlias;
import org.springframework.data.annotation.Version;
import org.springframework.data.couchbase.core.mapping.Document;
/**
* UserEncrypted entity for tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@Document
@TypeAlias(AbstractingTypeMapper.Type.ABSTRACTUSER)
public class UserEncrypted extends AbstractUser implements Serializable {
public UserEncrypted(){
this.subtype = AbstractingTypeMapper.Type.USER;
}
public UserEncrypted( final String lastname, final String encryptedField) {
this();
this.id = UUID.randomUUID().toString();
this.lastname = lastname;
this.encryptedField = encryptedField;
}
@PersistenceConstructor
public UserEncrypted(final String id, final String firstname, final String lastname) {
this();
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
}
public UserEncrypted(final String id, final String firstname, final String lastname, final String encryptedField) {
this();
this.id = id;
this.firstname = firstname;
this.lastname = lastname;
this.encryptedField = encryptedField;
}
@Version protected long version;
@Transient protected String transientInfo;
@CreatedBy protected String createdBy;
@CreatedDate protected long createdDate;
@LastModifiedBy protected String lastModifiedBy;
@LastModifiedDate protected long lastModifiedDate;
@Encrypted public String encryptedField;
@Encrypted public Integer encInteger=1;
@Encrypted public Long encLong=Long.valueOf(1);
@Encrypted public Boolean encBoolean = Boolean.TRUE;
List nicknames = List.of("Happy", "Sleepy");
Address homeAddress = new Address();
List<Address> addresses = new ArrayList<>();
@Encrypted
Address encAddress = new Address();
public String getLastname() {
return lastname;
}
public long getCreatedDate() {
return createdDate;
}
public void setCreatedDate(long createdDate) {
this.createdDate = createdDate;
}
public String getCreatedBy() {
return createdBy;
}
public void setCreatedBy(String createdBy) {
this.createdBy = createdBy;
}
public long getLastModifiedDate() {
return lastModifiedDate;
}
public String getLastModifiedBy() {
return lastModifiedBy;
}
public String getEncryptedField() {
return encryptedField;
}
public long getVersion() {
return version;
}
public void setVersion(long version) {
this.version = version;
}
public void setHomeAddress(Address address){
this.homeAddress = address;
}
public void setEncAddress(Address address){
this.encAddress = address;
}
public void addAddress(Address address){
this.addresses.add(address);
}
@Override
public int hashCode() {
return Objects.hash(getId(), firstname, lastname);
}
public String getTransientInfo() {
return transientInfo;
}
public void setTransientInfo(String something) {
transientInfo = something;
}
}

View File

@@ -0,0 +1,82 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.domain;
import java.util.List;
import java.util.stream.Stream;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.couchbase.repository.Collection;
import org.springframework.data.couchbase.repository.CouchbaseRepository;
import org.springframework.data.couchbase.repository.Query;
import org.springframework.data.couchbase.repository.ScanConsistency;
import org.springframework.data.couchbase.repository.Scope;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.query.QueryScanConsistency;
/**
* User Repository for tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@Repository
@ScanConsistency(query = QueryScanConsistency.REQUEST_PLUS)
public interface UserEncryptedRepository extends CouchbaseRepository<UserEncrypted, String> {
List<User> findByFirstname(String firstname);
List<User> findByFirstnameIgnoreCase(String firstname);
Stream<User> findByLastname(String lastname);
List<User> findByFirstnameIn(String... firstnames);
List<User> findByFirstnameIn(JsonArray firstnames);
List<User> findByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and firstname = $1 and lastname = $2")
List<User> getByFirstnameAndLastname(String firstname, String lastname);
@Query("#{#n1ql.selectEntity} where #{#n1ql.filter} and (firstname = $first or lastname = $last)")
List<User> getByFirstnameOrLastname(@Param("first") String firstname, @Param("last") String lastname);
List<User> findByIdIsNotNullAndFirstnameEquals(String firstname);
List<User> findByVersionEqualsAndFirstnameEquals(Long version, String firstname);
@Query("#{#n1ql.selectEntity}|#{#n1ql.filter}|#{#n1ql.bucket}|#{#n1ql.scope}|#{#n1ql.collection}")
@Scope("thisScope")
@Collection("thisCollection")
List<User> spelTests();
// simulate a slow operation
@Cacheable("mySpringCache")
default List<User> getByFirstname(String firstname) {
try {
Thread.sleep(1000 * 5);
} catch (InterruptedException ie) {}
return findByFirstname(firstname);
}
@Override
UserEncrypted save(UserEncrypted user);
}

View File

@@ -0,0 +1,221 @@
/*
* Copyright 2012-2022 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.couchbase.repository;
import static com.couchbase.client.core.util.CbCollections.mapOf;
import static com.couchbase.client.java.query.QueryScanConsistency.REQUEST_PLUS;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.Provider;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.couchbase.client.encryption.AeadAes256CbcHmacSha512Provider;
import com.couchbase.client.encryption.Decrypter;
import com.couchbase.client.encryption.DefaultCryptoManager;
import com.couchbase.client.encryption.Encrypter;
import com.couchbase.client.encryption.EncryptionResult;
import com.couchbase.client.encryption.Keyring;
import com.couchbase.client.java.encryption.annotation.Encrypted;
import com.couchbase.client.java.encryption.databind.jackson.EncryptionModule;
import com.couchbase.client.java.json.JsonArray;
import com.couchbase.client.java.json.JsonObject;
import com.couchbase.client.java.json.JsonValue;
import com.couchbase.client.java.json.JsonValueModule;
import com.couchbase.client.java.query.QueryOptions;
import com.couchbase.client.java.query.QueryScanConsistency;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.couchbase.config.AbstractCouchbaseConfiguration;
import org.springframework.data.couchbase.core.CouchbaseTemplate;
import org.springframework.data.couchbase.core.query.N1QLExpression;
import org.springframework.data.couchbase.core.query.N1QLQuery;
import org.springframework.data.couchbase.domain.Address;
import org.springframework.data.couchbase.domain.PersonValueRepository;
import org.springframework.data.couchbase.domain.User;
import org.springframework.data.couchbase.domain.UserEncrypted;
import org.springframework.data.couchbase.domain.UserEncryptedRepository;
import org.springframework.data.couchbase.domain.UserRepository;
import org.springframework.data.couchbase.repository.config.EnableCouchbaseRepositories;
import org.springframework.data.couchbase.util.ClusterAwareIntegrationTests;
import org.springframework.data.couchbase.util.ClusterType;
import org.springframework.data.couchbase.util.IgnoreWhen;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import com.couchbase.client.core.deps.io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import com.couchbase.client.core.encryption.CryptoManager;
import com.couchbase.client.core.env.SecurityConfig;
import com.couchbase.client.java.env.ClusterEnvironment;
import org.testcontainers.shaded.org.bouncycastle.asn1.isismtt.x509.AdditionalInformationSyntax;
/**
* Repository KV tests
*
* @author Michael Nitschinger
* @author Michael Reiche
*/
@SpringJUnitConfig(CouchbaseRepositoryFieldLevelEncryptionIntegrationTests.Config.class)
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
public class CouchbaseRepositoryFieldLevelEncryptionIntegrationTests extends ClusterAwareIntegrationTests {
@Autowired
UserEncryptedRepository userEncryptedRepository;
@Autowired PersonValueRepository personValueRepository;
@Autowired CouchbaseTemplate couchbaseTemplate;
@BeforeEach
public void beforeEach() {
super.beforeEach();
couchbaseTemplate.removeByQuery(UserEncrypted.class).withConsistency(REQUEST_PLUS).all();
couchbaseTemplate.findByQuery(UserEncrypted.class).withConsistency(REQUEST_PLUS).all();
}
@Test
void javaSDKEncryption() {
}
@Test
@IgnoreWhen(clusterTypes = ClusterType.MOCKED)
void saveAndFindById() {
UserEncrypted user = new UserEncrypted(UUID.randomUUID().toString(), "saveAndFindById", "l", "hello");
Address address = new Address(); // plaintext address with encrypted street
address.setEncStreet("Olcott Street");
address.setCity("Santa Clara");
user.addAddress(address);
user.setHomeAddress(address);
// cannot set encrypted fields within encrypted objects (i.e. setEncAddress())
Address encAddress = new Address(); // encrypted address with plaintext street.
encAddress.setStreet("Castro St");
encAddress.setCity("Mountain View");
user.setEncAddress(encAddress);
assertFalse(userEncryptedRepository.existsById(user.getId()));
userEncryptedRepository.save(user);
Optional<UserEncrypted> found = userEncryptedRepository.findById(user.getId());
assertTrue(found.isPresent());
System.err.println(found.get());
found.ifPresent(u -> assertEquals(user, u));
assertTrue(userEncryptedRepository.existsById(user.getId()));
//userEncryptedRepository.delete(user);
}
@Configuration
@EnableCouchbaseRepositories("org.springframework.data.couchbase")
static class Config extends AbstractCouchbaseConfiguration {
@Override
public String getConnectionString() {
return connectionString();
}
@Override
public String getUserName() {
return config().adminUsername();
}
@Override
public String getPassword() {
return config().adminPassword();
}
@Override
public String getBucketName() {
return bucketName();
}
@Override
protected void configureEnvironment(ClusterEnvironment.Builder builder) {
if (config().isUsingCloud()) {
builder.securityConfig(
SecurityConfig.builder().trustManagerFactory(InsecureTrustManagerFactory.INSTANCE).enableTls(true));
}
CryptoManager cryptoManager = cryptoManager();
builder.cryptoManager(cryptoManager).build();
}
@Override
protected CryptoManager cryptoManager() {
Decrypter decrypter = new Decrypter() {
@Override
public String algorithm() {
return "myAlg";
}
@Override
public byte[] decrypt(EncryptionResult encrypted) {
return Base64.getDecoder().decode(encrypted.getString("ciphertext"));
}
};
Encrypter encrypter = new Encrypter() {
@Override
public EncryptionResult encrypt(byte[] plaintext) {
return EncryptionResult
.fromMap(mapOf("alg", "myAlg", "ciphertext", Base64.getEncoder().encodeToString(plaintext)));
}
};
Map<String, byte[]> keyMap = new HashMap();
keyMap.put("myKey",
new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, });
Keyring keyring = Keyring.fromMap(keyMap);
// Provider secProvider;
AeadAes256CbcHmacSha512Provider provider = AeadAes256CbcHmacSha512Provider.builder().keyring(keyring)
/*.securityProvider(secProvider)*/.build();
return DefaultCryptoManager.builder().decrypter(provider.decrypter())
.defaultEncrypter(provider.encrypterForKey("myKey")).build();
}
byte[] hmacMe(String cbc_secret_key, String cbc_api_message) {
try {
return hmac("hmacSHA256", cbc_secret_key.getBytes("utf-8"), cbc_api_message.getBytes("utf-8"));
} catch (UnsupportedEncodingException | NoSuchAlgorithmException | InvalidKeyException ue) {
return null;
}
}
static byte[] hmac(String algorithm, byte[] key, byte[] message)
throws NoSuchAlgorithmException, InvalidKeyException {
Mac mac = Mac.getInstance(algorithm);
mac.init(new SecretKeySpec(key, algorithm));
return mac.doFinal(message);
}
}
}