Skip to content

Commit

Permalink
Add support for arrays, allow for encryption of arrays or collections…
Browse files Browse the repository at this point in the history
… of strings and limit allowed collection types to Set or List
  • Loading branch information
agrancaric committed May 6, 2022
1 parent 6ede80d commit d4b5083
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collector;
import java.util.stream.Collectors;

@Slf4j
Expand All @@ -45,11 +47,11 @@ public <T> T encryptData(T data, List<String> pathToEncryptDecryptList, Encrypti
return null;
}

if (CollectionUtils.isEmpty(pathToEncryptDecryptList) && data instanceof String) {
if (CollectionUtils.isEmpty(pathToEncryptDecryptList)) {
@SuppressWarnings("unchecked")
T encryptedText = (T) encryptDecryptText(encryptionContext, (String) data, EncryptionOperation.ENCRYPT);
T encryptedValue = (T) encryptDecryptValue(encryptionContext, data, EncryptionOperation.ENCRYPT);

return encryptedText;
return encryptedValue == null ? data : encryptedValue;
}

pathToEncryptDecryptList.forEach(path -> executeEncryptionOperation(encryptionContext, data, path, EncryptionOperation.ENCRYPT));
Expand All @@ -63,11 +65,11 @@ public <T> T decryptData(T data, List<String> pathToEncryptDecryptList, Encrypti
return null;
}

if (CollectionUtils.isEmpty(pathToEncryptDecryptList) && data instanceof String) {
if (CollectionUtils.isEmpty(pathToEncryptDecryptList)) {
@SuppressWarnings("unchecked")
T decryptedText = (T) encryptDecryptText(encryptionContext, (String) data, EncryptionOperation.DECRYPT);
T decryptedValue = (T) encryptDecryptValue(encryptionContext, data, EncryptionOperation.DECRYPT);

return decryptedText;
return decryptedValue == null ? data : decryptedValue;
}

pathToEncryptDecryptList.forEach(path -> executeEncryptionOperation(encryptionContext, data, path, EncryptionOperation.DECRYPT));
Expand All @@ -93,6 +95,9 @@ private void encryptDecryptNestedValue(EncryptionContext encryptionContext, Obje
if (objectContainingFieldsToEncryptOrDecrypt instanceof Collection) {
((Collection<?>) objectContainingFieldsToEncryptOrDecrypt).forEach(value -> encryptDecryptValue(encryptionContext, value, propertyName, operation));
}
else if (objectContainingFieldsToEncryptOrDecrypt instanceof Object[]) {
Arrays.stream(((Object[]) objectContainingFieldsToEncryptOrDecrypt)).forEach(value -> encryptDecryptValue(encryptionContext, value, propertyName, operation));
}
else {
encryptDecryptValue(encryptionContext, objectContainingFieldsToEncryptOrDecrypt, propertyName, operation);
}
Expand All @@ -101,24 +106,41 @@ private void encryptDecryptNestedValue(EncryptionContext encryptionContext, Obje

private void encryptDecryptValue(EncryptionContext encryptionContext, Object objectContainingFieldsToEncryptOrDecrypt, String propertyName, EncryptionOperation operation) {
Object value = getPropertyValueByPath(objectContainingFieldsToEncryptOrDecrypt, propertyName);
Object result = encryptDecryptValue(encryptionContext, value, operation);

if (result == null) {
return;
}

setPropertyValueByPath(objectContainingFieldsToEncryptOrDecrypt, propertyName, result);
}

private Object encryptDecryptValue(EncryptionContext encryptionContext, Object value, EncryptionOperation operation) {
if (value instanceof String) {
String textToEncryptOrDecrypt = (String) value;
String encryptedValue = encryptDecryptText(encryptionContext, textToEncryptOrDecrypt, operation);

setPropertyValueByPath(objectContainingFieldsToEncryptOrDecrypt, propertyName, encryptedValue);

return encryptDecryptText(encryptionContext, textToEncryptOrDecrypt, operation);
}
else if (value instanceof Collection && ((Collection<?>) value).stream().allMatch(String.class::isInstance)) {
else if (isSupportedCollection(value) && ((Collection<?>) value).stream().allMatch(String.class::isInstance)) {
@SuppressWarnings("unchecked")
Collection<String> textToEncryptOrDecryptList = (Collection<String>) value;
Collector<? super String, ?, ?> collector = value instanceof Set ? Collectors.toSet() : Collectors.toList();

Collection<String> encryptedValueList = textToEncryptOrDecryptList.stream()
return textToEncryptOrDecryptList.stream()
.map(textToEncryptOrDecrypt -> encryptDecryptText(encryptionContext, textToEncryptOrDecrypt, operation))
.collect(Collectors.toList());
.collect(collector);
}
else if (value instanceof String[]) {
String[] textToEncryptOrDecryptList = (String[]) value;

setPropertyValueByPath(objectContainingFieldsToEncryptOrDecrypt, propertyName, encryptedValueList);
return Arrays.stream(textToEncryptOrDecryptList)
.map(textToEncryptOrDecrypt -> encryptDecryptText(encryptionContext, textToEncryptOrDecrypt, operation))
.toArray(String[]::new);
}

log.warn("Unable to {} property, check if the specified path is correct and if the specified property is a string", operation.name().toLowerCase());

return null;
}

@SuppressWarnings("unchecked")
Expand All @@ -145,6 +167,10 @@ protected void setPropertyValueByPath(Object holder, String path, Object value)
}
}

private boolean isSupportedCollection(Object value) {
return value instanceof List || value instanceof Set;
}

private String encryptDecryptText(EncryptionContext encryptionContext, String text, EncryptionOperation operation) {
log.debug("Starting encryption operation: {} for method: {}", operation.name(), encryptionContext.getFullyQualifiedMethodName());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import net.croz.nrich.encrypt.EncryptTestConfiguration;
import net.croz.nrich.encrypt.api.model.EncryptionContext;
import net.croz.nrich.encrypt.service.stub.DataEncryptionServiceArrayTestObject;
import net.croz.nrich.encrypt.service.stub.DataEncryptionServiceNestedTestObject;
import net.croz.nrich.encrypt.service.stub.DataEncryptionServiceTestObject;
import org.junit.jupiter.api.Test;
Expand All @@ -30,57 +31,60 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.catchThrowable;

@SpringJUnitConfig(EncryptTestConfiguration.class)
class DefaultDataEncryptServiceTest {

private static final String TEXT_TO_ENCRYPT_DECRYPT = "some text";

@Autowired
private DefaultDataEncryptService dataEncryptionService;

@Test
void shouldEncryptSimpleData() {
// given
List<String> propertyList = Collections.singletonList("propertyToEncryptDecrypt");
String textToEncrypt = "some text";
DataEncryptionServiceTestObject data = new DataEncryptionServiceTestObject();

data.setPropertyToEncryptDecrypt(textToEncrypt);
data.setPropertyToEncryptDecrypt(TEXT_TO_ENCRYPT_DECRYPT);

// when
DataEncryptionServiceTestObject result = dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(result).isNotNull();
assertThat(result.getPropertyToEncryptDecrypt()).isNotEqualTo(textToEncrypt);
assertThat(result.getPropertyToEncryptDecrypt()).isNotEqualTo(TEXT_TO_ENCRYPT_DECRYPT);
}

@Test
void shouldDecryptSimpleData() {
// given
List<String> propertyList = Collections.singletonList("propertyToEncryptDecrypt");
String textToEncrypt = "some text";
DataEncryptionServiceTestObject data = new DataEncryptionServiceTestObject();

data.setPropertyToEncryptDecrypt(textToEncrypt);
data.setPropertyToEncryptDecrypt(TEXT_TO_ENCRYPT_DECRYPT);

// when
DataEncryptionServiceTestObject result = dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(result).isNotNull();
assertThat(result.getPropertyToEncryptDecrypt()).isNotEqualTo(textToEncrypt);
assertThat(result.getPropertyToEncryptDecrypt()).isNotEqualTo(TEXT_TO_ENCRYPT_DECRYPT);

// and when
DataEncryptionServiceTestObject decryptResult = dataEncryptionService.decryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(decryptResult).isNotNull();
assertThat(decryptResult.getPropertyToEncryptDecrypt()).isEqualTo(textToEncrypt);
assertThat(decryptResult.getPropertyToEncryptDecrypt()).isEqualTo(TEXT_TO_ENCRYPT_DECRYPT);
}

@Test
Expand Down Expand Up @@ -213,34 +217,32 @@ void shouldEncryptDecryptMapData() {
// given
String key = "mapKey";
List<String> propertyList = Collections.singletonList(key);
String textToEncrypt = "some text";
Map<String, String> data = new HashMap<>();

data.put(key, textToEncrypt);
data.put(key, TEXT_TO_ENCRYPT_DECRYPT);

// when
Map<String, String> result = dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(result).doesNotContainEntry(key, textToEncrypt);
assertThat(result).doesNotContainEntry(key, TEXT_TO_ENCRYPT_DECRYPT);

// and when
Map<String, String> decryptResult = dataEncryptionService.decryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(decryptResult).containsEntry(key, textToEncrypt);
assertThat(decryptResult).containsEntry(key, TEXT_TO_ENCRYPT_DECRYPT);
}

@Test
void shouldEncryptDecryptNestedMapData() {
// given
String key = "mapKey";
List<String> propertyList = Collections.singletonList("mapKey.mapKey");
String textToEncrypt = "some text";
Map<String, Map<String, String>> data = new HashMap<>();
Map<String, String> nestedData = new HashMap<>();

nestedData.put(key, textToEncrypt);
nestedData.put(key, TEXT_TO_ENCRYPT_DECRYPT);

data.put(key, nestedData);

Expand All @@ -249,13 +251,121 @@ void shouldEncryptDecryptNestedMapData() {

// then
assertThat(result).isNotNull();
assertThat(result.get(key)).doesNotContainEntry(key, textToEncrypt);
assertThat(result.get(key)).doesNotContainEntry(key, TEXT_TO_ENCRYPT_DECRYPT);

// and when
Map<String, Map<String, String>> decryptResult = dataEncryptionService.decryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(decryptResult).isNotNull();
assertThat(result.get(key)).containsEntry(key, textToEncrypt);
assertThat(result.get(key)).containsEntry(key, TEXT_TO_ENCRYPT_DECRYPT);
}

@Test
void shouldEncryptDecryptArrayData() {
// given
DataEncryptionServiceArrayTestObject testObject = new DataEncryptionServiceArrayTestObject();
List<String> propertyList = Arrays.asList("propertyEncryptDecrypt", "arrayEncrypt");
DataEncryptionServiceArrayTestObject[] data = new DataEncryptionServiceArrayTestObject[] { testObject };

testObject.setArrayEncrypt(new String[] { TEXT_TO_ENCRYPT_DECRYPT });
testObject.setPropertyEncryptDecrypt(TEXT_TO_ENCRYPT_DECRYPT);

// when
DataEncryptionServiceArrayTestObject[] result = dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(result).isNotNull();
assertThat(result[0].getPropertyEncryptDecrypt()).isNotEqualTo(TEXT_TO_ENCRYPT_DECRYPT);
assertThat(result[0].getArrayEncrypt()).isNotEmpty();
assertThat(result[0].getArrayEncrypt()[0]).isNotEqualTo(TEXT_TO_ENCRYPT_DECRYPT);

// and when
DataEncryptionServiceArrayTestObject[] decryptResult = dataEncryptionService.decryptData(result, propertyList, EncryptionContext.builder().build());

// then
assertThat(decryptResult).usingRecursiveComparison().isEqualTo(data);
}

@Test
void shouldNotFailWithUnsupportedTypes() {
// given
int[] data = new int[] { 1 };
List<String> propertyList = Collections.singletonList("property");

// when
Throwable thrown = catchThrowable(() -> dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build()));

// then
assertThat(thrown).isNull();
}

@Test
void shouldEncryptDecryptStringCollection() {
// given
List<String> propertyList = Collections.emptyList();
List<String> data = Collections.singletonList(TEXT_TO_ENCRYPT_DECRYPT);

// when
List<String> result = dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(result).isNotEmpty().doesNotContain(TEXT_TO_ENCRYPT_DECRYPT);

// and when
List<String> decryptResult = dataEncryptionService.decryptData(result, propertyList, EncryptionContext.builder().build());

// then
assertThat(decryptResult).containsExactly(TEXT_TO_ENCRYPT_DECRYPT);
}

@Test
void shouldEncryptDecryptStringArray() {
// given
List<String> propertyList = Collections.emptyList();
String[] data = new String[] { TEXT_TO_ENCRYPT_DECRYPT };

// when
String[] result = dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(result).isNotEmpty().doesNotContain(TEXT_TO_ENCRYPT_DECRYPT);

// and when
String[] decryptResult = dataEncryptionService.decryptData(result, propertyList, EncryptionContext.builder().build());

// then
assertThat(decryptResult).containsExactly(TEXT_TO_ENCRYPT_DECRYPT);
}

@Test
void shouldEncryptDecryptStringSet() {
// given
List<String> propertyList = Collections.emptyList();
Set<String> data = new HashSet<>(Collections.singleton(TEXT_TO_ENCRYPT_DECRYPT));

// when
Set<String> result = dataEncryptionService.encryptData(data, propertyList, EncryptionContext.builder().build());

// then
assertThat(result).isNotEmpty().doesNotContain(TEXT_TO_ENCRYPT_DECRYPT);

// and when
Set<String> decryptResult = dataEncryptionService.decryptData(result, propertyList, EncryptionContext.builder().build());

// then
assertThat(decryptResult).containsExactly(TEXT_TO_ENCRYPT_DECRYPT);
}

@Test
void shouldNotFailForNonStringCollectionArray() {
// given
Object[] data = new Object[] { "value", Integer.MAX_VALUE };

// when
Throwable thrown = catchThrowable(() -> dataEncryptionService.encryptData(data, Collections.emptyList(), EncryptionContext.builder().build()));

// then
assertThat(thrown).isNull();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2020-2022 CROZ d.o.o, 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 net.croz.nrich.encrypt.service.stub;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
public class DataEncryptionServiceArrayTestObject {

private String propertyEncryptDecrypt;

private String[] arrayEncrypt;

}

0 comments on commit d4b5083

Please sign in to comment.