javaencryptionaesobjectmapperfasterxml

Is it possible create an ObjectMapper configured such that it AES encrypts all fields except fields annotated with a certain annotation?


Let's say you have an annotation configured on a per field basis:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface DoNotEncrypt {
}

OK so now you have the annotation. So you can have your fields annotated when you don't want them encrypted:

static class CoolStuff {
        private String shouldBeEncrypted;
        @DoNotEncrypt
        private String notEncrypted = "should not be encrypted";

        public String getShouldBeEncrypted() {
            return shouldBeEncrypted;
        }

        public void setShouldBeEncrypted(String shouldBeEncrypted) {
            this.shouldBeEncrypted = shouldBeEncrypted;
        }

        public String getNotEncrypted() {
            return notEncrypted;
        }

        public void setNotEncrypted(String notEncrypted) {
            this.notEncrypted = notEncrypted;
        }
    }

I want to implement this serializer:

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Encrypt every string except fields that have annotation {@link DoNotEncrypt}
 */
public class ConfidentialSerializer extends JsonSerializer<Object> {
    private static final Logger LOG = LoggerFactory.getLogger(ConfidentialSerializer.class);
    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) {
       // I want to implement this here
    }
    public static ObjectMapper configurationObjectMapper() {
        ObjectMapper objectMapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        // is this right?
        module.addSerializer(Object.class, new ConfidentialSerializer());
        objectMapper.registerModule(module);
        return objectMapper;
    }
}

In order to test the change i want to see that strings are encrypted unless annotationed @DoNotEncrypt.

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;

public class ConfidentialSerializerTest {
    static class CoolStuff {
        private String shouldBeEncrypted;
        @DoNotEncrypt
        private String notEncrypted;

        public String getShouldBeEncrypted() {
            return shouldBeEncrypted;
        }

        public void setShouldBeEncrypted(String shouldBeEncrypted) {
            this.shouldBeEncrypted = shouldBeEncrypted;
        }

        public String getNotEncrypted() {
            return notEncrypted;
        }

        public void setNotEncrypted(String notEncrypted) {
            this.notEncrypted = notEncrypted;
        }
    }
    @Test
    public void testConfigurationObjectMapper() throws JsonProcessingException {
        ObjectMapper objectMapper = ConfidentialSerializer.configurationObjectMapper();
        CoolStuff coolStuff = new CoolStuff();
        coolStuff = new CoolStuff();
        coolStuff.setShouldBeEncrypted("is");
        coolStuff.setNotEncrypted("is not");
        String out = objectMapper.writeValueAsString(coolStuff);
        Map mapOut = objectMapper.readValue(out, Map.class);
        Assert.assertNotEquals("is", mapOut.get("notEncrypted"));
        Assert.assertEquals("is not", mapOut.get("shouldBeEncrypted"));    }
}

Is this something that is possible with Object Mapper? If so, how do you do it?


Solution

  • I think I have it working now.

    The key is recursion in ConfidentialSerializer. Once you have that working the rest is pretty easy.

    DoNotEncrypt.java

    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    public @interface DoNotEncrypt {
    }
    

    ConfidentialSerializerTest.java

    import com.fasterxml.jackson.core.JsonGenerator;
    import com.fasterxml.jackson.databind.JsonSerializer;
    import com.fasterxml.jackson.databind.SerializerProvider;
    import org.jetbrains.annotations.Nullable;
    
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.util.Arrays;
    import java.util.Collection;
    import java.util.Map;
    
    public class ConfidentialSerializer extends JsonSerializer<Object> {
        @Override
        public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
            try {
                if (value == null) {
                    gen.writeNull();
                    return;
                }
    
                Class<?> clazz = value.getClass();
                // Write String values directly
                if (value instanceof String) {
                    gen.writeString((String) value);
                    return;
                }
                if (value instanceof Long) {
                    gen.writeNumber((Long) value);
                    return;
                }
                if (value instanceof Integer) {
                    gen.writeNumber((Integer) value);
                    return;
                }
                if (value instanceof Short) {
                    gen.writeNumber((Short) value);
                    return;
                }
                if (value instanceof Boolean) {
                    gen.writeBoolean((Boolean) value);
                    return;
                }
                if (value instanceof Collection) {
                    gen.writeStartArray();
                    for (Object item : (Collection<?>) value) {
                        serialize(item, gen, serializers); // Recursively process list elements
                    }
                    gen.writeEndArray();
                    return;
                }
                if (value instanceof Map) {
                    gen.writeStartObject();
                    for (Map.Entry<?, ?> entry : ((Map<?, ?>) value).entrySet()) {
                        gen.writeFieldName(entry.getKey().toString());
                        serialize(entry.getValue(), gen, serializers); // Recursively process map values
                    }
                    gen.writeEndObject();
                    return;
                }
                gen.writeStartObject();
    
                for (Field field : clazz.getDeclaredFields()) {
                    Method getterMethod = lookupGetterMethod(value, field);
                    if (getterMethod != null) {
                        Object fieldValue = getFieldValue(getterMethod, field.getName(), value);
    
                        gen.writeFieldName(field.getName());
    
                        // Recursively process child fields
                        if (fieldValue != null) {
                            if (field.isAnnotationPresent(Confidential.class) && fieldValue instanceof String) {
                                fieldValue = AESEncrypter.encrypt((String) fieldValue);
                            }
                            serialize(fieldValue, gen, serializers);
                        } else {
                            gen.writeNull();
                        }
                    }
                }
                gen.writeEndObject();
            } catch (Exception e) {
                throw new RuntimeException("Error encrypting nested field", e);
            }
        }
    
        private static @Nullable Method lookupGetterMethod(Object value, Field field) {
            String fieldName = field.getName();
            String methodNameGet = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
            String methodNameIs = "is" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
            Method getterMethod = Arrays.stream(value.getClass().getMethods())
                    .filter(method -> method.getParameterCount() == 0 &&
                            method.getModifiers() == java.lang.reflect.Modifier.PUBLIC &&
                            (method.getName().equals(methodNameGet) || method.getName().equals(methodNameIs)))
                    .findFirst().orElse(null);
            return getterMethod;
        }
    
        public Object getFieldValue(Method getterMethod, String fieldName, Object object) {
            try {
                return getterMethod.invoke(object);
            } catch (IllegalAccessException | InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        }
    }