Skip to content

Commit fe2eeae

Browse files
committed
FLE implemenation for Spring Data Couchbase.
Closes #763.
1 parent eedad43 commit fe2eeae

File tree

10 files changed

+906
-239
lines changed

10 files changed

+906
-239
lines changed

spring-data-couchbase/src/main/java/org/springframework/data/couchbase/config/AbstractCouchbaseConfiguration.java

-1
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ public ClusterEnvironment couchbaseClusterEnvironment() {
168168
* @param builder the builder that can be customized.
169169
*/
170170
protected void configureEnvironment(final ClusterEnvironment.Builder builder) {
171-
172171
}
173172

174173
@Bean(name = BeanNames.COUCHBASE_TEMPLATE)

spring-data-couchbase/src/main/java/org/springframework/data/couchbase/core/convert/AbstractCouchbaseConverter.java

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.Collections;
2020

21+
import com.couchbase.client.java.query.QueryScanConsistency;
2122
import org.springframework.beans.factory.InitializingBean;
2223
import org.springframework.core.convert.ConversionService;
2324
import org.springframework.core.convert.TypeDescriptor;

spring-data-couchbase/src/main/java/org/springframework/data/couchbase/core/convert/CryptoConverter.java

+197-88
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,31 @@
1515
*/
1616
package org.springframework.data.couchbase.core.convert;
1717

18-
import java.lang.reflect.Constructor;
19-
import java.lang.reflect.InvocationTargetException;
2018
import java.nio.charset.StandardCharsets;
2119
import java.util.LinkedList;
2220
import java.util.List;
21+
import java.util.Locale;
2322
import java.util.Map;
23+
import java.util.Optional;
2424

25+
import org.springframework.core.convert.ConversionFailedException;
2526
import org.springframework.core.convert.ConversionService;
27+
import org.springframework.core.convert.ConverterNotFoundException;
28+
import org.springframework.data.convert.CustomConversions;
2629
import org.springframework.data.convert.PropertyValueConverter;
2730
import org.springframework.data.convert.ValueConversionContext;
2831
import org.springframework.data.couchbase.core.mapping.CouchbaseDocument;
2932
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentEntity;
3033
import org.springframework.data.couchbase.core.mapping.CouchbasePersistentProperty;
3134
import org.springframework.data.mapping.PersistentProperty;
35+
import org.springframework.data.mapping.model.ConvertingPropertyAccessor;
3236
import org.springframework.util.Assert;
3337

3438
import com.couchbase.client.core.encryption.CryptoManager;
39+
import com.couchbase.client.core.error.InvalidArgumentException;
40+
import com.couchbase.client.java.json.JsonArray;
3541
import com.couchbase.client.java.json.JsonObject;
42+
import com.couchbase.client.java.json.JsonValue;
3643

3744
/**
3845
* Encrypt/Decrypted properties annotated with
@@ -54,114 +61,216 @@ public Object read(CouchbaseDocument value, ValueConversionContext<? extends Per
5461
if (decrypted == null) {
5562
return null;
5663
}
57-
// it's decrypted into a String. Now figure out how to convert to the property type.
64+
// it's decrypted to byte[]. Now figure out how to convert to the property type.
65+
return coerceToValueRead(decrypted, (CouchbaseConversionContext) context);
66+
}
67+
68+
@Override
69+
public CouchbaseDocument write(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
5870
CouchbaseConversionContext ctx = (CouchbaseConversionContext) context;
5971
CouchbasePersistentProperty property = ctx.getProperty();
60-
org.springframework.data.convert.CustomConversions conversions = ctx.getConverter().getConversions();
61-
ConversionService svc = ctx.getConverter().conversionService;
72+
byte[] plainText = coerceToBytesWrite(property, ctx.getAccessor(), ctx);
73+
Map<String, Object> encrypted = cryptoManager().encrypt(plainText, CryptoManager.DEFAULT_ENCRYPTER_ALIAS);
74+
return new CouchbaseDocument().setContent(encrypted);
75+
}
76+
77+
private Object coerceToValueRead(byte[] decrypted, CouchbaseConversionContext context) {
78+
CouchbasePersistentProperty property = context.getProperty();
79+
80+
CustomConversions cnvs = context.getConverter().getConversions();
81+
ConversionService svc = context.getConverter().getConversionService();
82+
Class<?> type = property.getType();
6283

63-
boolean wasString = false;
64-
List<Exception> exceptionList = new LinkedList<>();
6584
String decryptedString = new String(decrypted);
66-
if (conversions.isSimpleType(property.getType())
67-
|| conversions.hasCustomReadTarget(String.class, context.getProperty().getType())) {
68-
if (decryptedString.startsWith("\"") && decryptedString.endsWith("\"")) {
69-
decryptedString = decryptedString.substring(1, decryptedString.length() - 1);
70-
decryptedString = decryptedString.replaceAll("\\\"", "\"");
71-
wasString = true;
72-
}
85+
if ("null".equals(decryptedString)) {
86+
return null;
7387
}
88+
/* this what we would do if we could use a JsonParser with a beanPropertyTypeRef
89+
final JsonParser plaintextParser = p.getCodec().getFactory().createParser(plaintext);
90+
plaintextParser.setCodec(p.getCodec());
91+
92+
return plaintextParser.readValueAs(beanPropertyTypeRef);
93+
*/
7494

75-
if (conversions.isSimpleType(property.getType())) {
76-
if (wasString && conversions.hasCustomReadTarget(String.class, context.getProperty().getType())) {
77-
try {
78-
return svc.convert(decryptedString, context.getProperty().getType());
79-
} catch (Exception e) {
80-
exceptionList.add(e);
81-
}
82-
}
83-
if (conversions.hasCustomReadTarget(Long.class, context.getProperty().getType())) {
84-
try {
85-
return svc.convert(Long.valueOf(decryptedString), property.getType());
86-
} catch (Exception e) {
87-
exceptionList.add(e);
88-
}
89-
}
90-
if (conversions.hasCustomReadTarget(Double.class, context.getProperty().getType())) {
91-
try {
92-
return svc.convert(Double.valueOf(decryptedString), property.getType());
93-
} catch (Exception e) {
94-
exceptionList.add(e);
95-
}
96-
}
97-
if (conversions.hasCustomReadTarget(Boolean.class, context.getProperty().getType())) {
98-
try {
99-
Object booleanResult = svc.convert(Boolean.valueOf(decryptedString), property.getType());
100-
if (booleanResult == null) {
101-
throw new Exception("value " + decryptedString + " would not convert to boolean");
102-
}
103-
} catch (Exception e) {
104-
exceptionList.add(e);
105-
}
106-
}
107-
// let's try to find a constructor...
108-
try {
109-
Constructor<?> constructor = context.getProperty().getType().getConstructor(String.class);
110-
return constructor.newInstance(decryptedString);
111-
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
112-
exceptionList.add(new Exception("tried to instantiate from constructor taking string arg but got " + e));
113-
}
114-
// last chance...
95+
if (!cnvs.isSimpleType(type) && !type.isArray()) {
96+
JsonObject jo = JsonObject.fromJson(decryptedString);
97+
CouchbaseDocument source = new CouchbaseDocument().setContent(jo);
98+
return context.getConverter().read(property.getTypeInformation(), source);
99+
} else {
100+
String jsonString = "{\"" + property.getFieldName() + "\":" + decryptedString + "}";
115101
try {
116-
return ctx.getConverter().getPotentiallyConvertedSimpleRead(decryptedString, property);
117-
} catch (Exception e) {
118-
exceptionList.add(e);
119-
RuntimeException ee = new RuntimeException(
120-
"failed to convert " + decryptedString + " due to the following suppressed reasons(s): ");
121-
exceptionList.stream().forEach(ee::addSuppressed);
122-
throw ee;
102+
CouchbaseDocument decryptedDoc = new CouchbaseDocument().setContent(JsonObject.fromJson(jsonString));
103+
return context.getConverter().getPotentiallyConvertedSimpleRead(decryptedDoc.get(property.getFieldName()),
104+
property);
105+
} catch (InvalidArgumentException | ConverterNotFoundException | ConversionFailedException e) {
106+
throw new RuntimeException(decryptedString, e);
123107
}
124-
125-
} else {
126-
CouchbaseDocument decryptedDoc = new CouchbaseDocument().setContent(JsonObject.fromJson(decrypted));
127-
CouchbasePersistentEntity<?> entity = ctx.getConverter().getMappingContext()
128-
.getRequiredPersistentEntity(property.getType());
129-
return ctx.getConverter().read(entity, decryptedDoc, null);
130108
}
131109
}
132110

133-
@Override
134-
public CouchbaseDocument write(Object value, ValueConversionContext<? extends PersistentProperty<?>> context) {
111+
private byte[] coerceToBytesWrite(CouchbasePersistentProperty property, ConvertingPropertyAccessor accessor,
112+
CouchbaseConversionContext context) {
135113
byte[] plainText;
136-
CouchbaseConversionContext ctx = (CouchbaseConversionContext) context;
137-
CouchbasePersistentProperty property = ctx.getProperty();
138-
org.springframework.data.convert.CustomConversions conversions = ctx.getConverter().getConversions();
139-
140-
Class<?> sourceType = context.getProperty().getType();
141-
Class<?> targetType = conversions.getCustomWriteTarget(context.getProperty().getType()).orElse(null);
114+
CustomConversions cnvs = context.getConverter().getConversions();
142115

143-
value = ctx.getConverter().getPotentiallyConvertedSimpleWrite(property, ctx.getAccessor(), false);
144-
if (conversions.isSimpleType(sourceType)) {
145-
String plainString;
146-
plainString = (String) value;
147-
if (sourceType == String.class || targetType == String.class) {
116+
Class<?> sourceType = property.getType();
117+
Class<?> targetType = cnvs.getCustomWriteTarget(property.getType()).orElse(null);
118+
Object value = context.getConverter().getPotentiallyConvertedSimpleWrite(property, accessor, false);
119+
if (value == null) { // null
120+
plainText = "null".getBytes(StandardCharsets.UTF_8);
121+
} else if (value.getClass().isArray()) { // array
122+
JsonArray ja;
123+
if (value.getClass().getComponentType().isPrimitive()) {
124+
ja = jaFromPrimitiveArray(value);
125+
} else {
126+
ja = jaFromObjectArray(value, context.getConverter());
127+
}
128+
plainText = ja.toBytes();
129+
} else if (cnvs.isSimpleType(sourceType)) { // simpleType
130+
String plainString = value != null ? value.toString() : null;
131+
if ((sourceType == String.class || targetType == String.class) || sourceType == Character.class
132+
|| sourceType == char.class || Enum.class.isAssignableFrom(sourceType)
133+
|| Locale.class.isAssignableFrom(sourceType)) {
134+
// TODO use jackson serializer here
148135
plainString = "\"" + plainString.replaceAll("\"", "\\\"") + "\"";
149136
}
150137
plainText = plainString.getBytes(StandardCharsets.UTF_8);
151-
} else {
138+
} else { // an entity
152139
plainText = JsonObject.fromJson(context.read(value).toString().getBytes(StandardCharsets.UTF_8)).toBytes();
153140
}
154-
Map<String, Object> encrypted = cryptoManager().encrypt(plainText, CryptoManager.DEFAULT_ENCRYPTER_ALIAS);
155-
CouchbaseDocument encryptedDoc = new CouchbaseDocument();
156-
for (Map.Entry<String, Object> entry : encrypted.entrySet()) {
157-
encryptedDoc.put(entry.getKey(), entry.getValue());
158-
}
159-
return encryptedDoc;
141+
return plainText;
160142
}
161143

162144
CryptoManager cryptoManager() {
163-
Assert.notNull(cryptoManager, "cryptoManager is null");
145+
Assert.notNull(cryptoManager,
146+
"cryptoManager needed to encrypt/decrypt but it is null. Override needed for cryptoManager() method of "
147+
+ AbstractCouchbaseConverter.class.getName());
164148
return cryptoManager;
165149
}
166150

151+
JsonArray jaFromObjectArray(Object value, MappingCouchbaseConverter converter) {
152+
CustomConversions cnvs = converter.getConversions();
153+
ConversionService svc = converter.getConversionService();
154+
JsonArray ja = JsonArray.ja();
155+
for (Object o : (Object[]) value) {
156+
ja.add(coerceToJson(o, cnvs, svc));
157+
}
158+
return ja;
159+
}
160+
161+
JsonArray jaFromPrimitiveArray(Object value) {
162+
Class<?> component = value.getClass().getComponentType();
163+
JsonArray jArray;
164+
if (Long.TYPE.isAssignableFrom(component)) {
165+
jArray = ja_long((long[]) value);
166+
} else if (Integer.TYPE.isAssignableFrom(component)) {
167+
jArray = ja_int((int[]) value);
168+
} else if (Double.TYPE.isAssignableFrom(component)) {
169+
jArray = ja_double((double[]) value);
170+
} else if (Float.TYPE.isAssignableFrom(component)) {
171+
jArray = ja_float((float[]) value);
172+
} else if (Boolean.TYPE.isAssignableFrom(component)) {
173+
jArray = ja_boolean((boolean[]) value);
174+
} else if (Short.TYPE.isAssignableFrom(component)) {
175+
jArray = ja_short((short[]) value);
176+
} else if (Byte.TYPE.isAssignableFrom(component)) {
177+
jArray = ja_byte((byte[]) value);
178+
} else if (Character.TYPE.isAssignableFrom(component)) {
179+
jArray = ja_char((char[]) value);
180+
} else {
181+
throw new RuntimeException("unhandled primitive array: " + component.getName());
182+
}
183+
return jArray;
184+
}
185+
186+
JsonArray ja_long(long[] array) {
187+
JsonArray ja = JsonArray.ja();
188+
for (long t : array) {
189+
ja.add(t);
190+
}
191+
return ja;
192+
}
193+
194+
JsonArray ja_int(int[] array) {
195+
JsonArray ja = JsonArray.ja();
196+
for (int t : array) {
197+
ja.add(t);
198+
}
199+
return ja;
200+
}
201+
202+
JsonArray ja_double(double[] array) {
203+
JsonArray ja = JsonArray.ja();
204+
for (double t : array) {
205+
ja.add(t);
206+
}
207+
return ja;
208+
}
209+
210+
JsonArray ja_float(float[] array) {
211+
JsonArray ja = JsonArray.ja();
212+
for (float t : array) {
213+
ja.add(t);
214+
}
215+
return ja;
216+
}
217+
218+
JsonArray ja_boolean(boolean[] array) {
219+
JsonArray ja = JsonArray.ja();
220+
for (boolean t : array) {
221+
ja.add(t);
222+
}
223+
return ja;
224+
}
225+
226+
JsonArray ja_short(short[] array) {
227+
JsonArray ja = JsonArray.ja();
228+
for (short t : array) {
229+
ja.add(t);
230+
}
231+
return ja;
232+
}
233+
234+
JsonArray ja_byte(byte[] array) {
235+
JsonArray ja = JsonArray.ja();
236+
for (byte t : array) {
237+
ja.add(t);
238+
}
239+
return ja;
240+
}
241+
242+
JsonArray ja_char(char[] array) {
243+
JsonArray ja = JsonArray.ja();
244+
for (char t : array) {
245+
ja.add(String.valueOf(t));
246+
}
247+
return ja;
248+
}
249+
250+
Object coerceToJson(Object o, CustomConversions cnvs, ConversionService svc) {
251+
if (o != null && o.getClass() == Optional.class) {
252+
o = ((Optional<?>) o).isEmpty() ? null : ((Optional) o).get();
253+
}
254+
Optional<Class<?>> clazz;
255+
if (o == null) {
256+
o = JsonValue.NULL;
257+
} else if ((clazz = cnvs.getCustomWriteTarget(o.getClass())).isPresent()) {
258+
o = svc.convert(o, clazz.get());
259+
} else if (JsonObject.checkType(o)) {
260+
// The object is of an acceptable type
261+
} else if (Number.class.isAssignableFrom(o.getClass())) {
262+
if (o.toString().contains(".")) {
263+
o = ((Number) o).doubleValue();
264+
} else {
265+
o = ((Number) o).longValue();
266+
}
267+
} else if (Character.class.isAssignableFrom(o.getClass())) {
268+
o = ((Character) o).toString();
269+
} else if (Enum.class.isAssignableFrom(o.getClass())) {
270+
o = ((Enum) o).name();
271+
} else { // punt
272+
o = o.toString();
273+
}
274+
return o;
275+
}
167276
}

0 commit comments

Comments
 (0)