6

I am trying to mask sensitive data while serializing using jackson.

I have tried using @JsonSerialize and a custom annotation @Mask .

Mask.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 Mask {
  String value() default "XXX-DEFAULT MASK FORMAT-XXX";
}

Employee.java

import com.fasterxml.jackson.databind.annotation.JsonSerialize;

import java.util.Map;

public class Employee {

  @Mask(value = "*** The value of this attribute is masked for security reason ***")
  @JsonSerialize(using = MaskStringValueSerializer.class)
  protected String name;

  @Mask
  @JsonSerialize(using = MaskStringValueSerializer.class)
  protected String empId;

  @JsonSerialize(using = MaskMapStringValueSerializer.class)
  protected Map<Category, String> categoryMap;

  public Employee() {
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getEmpId() {
    return empId;
  }

  public void setEmpId(String empId) {
    this.empId = empId;
  }

  public Map<Category, String> getCategoryMap() {
    return categoryMap;
  }

  public void setCategoryMap(Map<Category, String> categoryMap) {
    this.categoryMap = categoryMap;
  }
}

Category.java

public enum Category {
  @Mask
  CATEGORY1,
  @Mask(value = "*** This value of this attribute is masked for security reason ***")
  CATEGORY2,
  CATEGORY3;
}

MaskMapStringValueSerializer.java

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;

import java.io.IOException;
import java.util.Map;

public class MaskMapStringValueSerializer extends JsonSerializer<Map<Category, String>> {

  @Override
  public void serialize(Map<Category, String> map, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    jsonGenerator.writeStartObject();

    for (Category key : map.keySet()) {
      Mask annot = null;
      try {
        annot = key.getClass().getField(key.name()).getAnnotation(Mask.class);
      } catch (NoSuchFieldException e) {
        e.printStackTrace();
      }

      if (annot != null) {
        jsonGenerator.writeStringField(((Category) key).name(), annot.value());
      } else {
        jsonGenerator.writeObjectField(((Category) key).name(), map.get(key));
      }
    }

    jsonGenerator.writeEndObject();

  }
}

MaskStringValueSerializer.java

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

import java.io.IOException;

public class MaskStringValueSerializer extends StdSerializer<String> implements ContextualSerializer {
  private Mask annot;

  public MaskStringValueSerializer() {
    super(String.class);
  }

  public MaskStringValueSerializer(Mask logMaskAnnotation) {
    super(String.class);
    this.annot = logMaskAnnotation;
  }

  public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
    if (annot != null && s != null && !s.isEmpty()) {
      jsonGenerator.writeString(annot.value());
    } else {
      jsonGenerator.writeString(s);
    }
  }

  public JsonSerializer<?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
    Mask annot = null;
    if (beanProperty != null) {
      annot = beanProperty.getAnnotation(Mask.class);
    }
    return new MaskStringValueSerializer(annot);

  }
}

MaskValueTest.java

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;
import java.util.Map;

public class MaskValueTest {


  public static void main(String args[]) throws Exception{
    Employee employee = new Employee();

    employee.setName("John Doe");
    employee.setEmpId("1234567890");
    Map<Category, String> catMap = new HashMap<>();
    catMap.put(Category.CATEGORY1, "CATEGORY1");
    catMap.put(Category.CATEGORY2, "CATEGORY2");
    catMap.put(Category.CATEGORY3, "CATEGORY3");
    employee.setCategoryMap(catMap);

    ObjectMapper mapper = new ObjectMapper();
    System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));
  }
}

Output -

{
  "name" : "*** The value of this attribute is masked for security reason ***",
  "empId" : "XXX-DEFAULT MASK FORMAT-XXX",
  "categoryMap" : {
    "CATEGORY1" : "XXX-DEFAULT MASK FORMAT-XXX",
    "CATEGORY2" : "*** The value of this attribute is masked for security reason ***",
    "CATEGORY3" : "CATEGORY3"
  }
}
  • The result is as per expectation, however, this seems to be static masking.
  • The intention was to mask only when needed, e.g. while printing in the logs where the all these sensitive data should be masked.
  • If I have to send this json for document indexing where the values should be as it is, this implementation fails.

I am looking for an Annotation based solution, where I can use 2 different instance of ObjectMapper initialized with JsonSerializers.

Samar
  • 69
  • 1
  • 1
  • 5

4 Answers4

3

This can be an implementation for what Andreas suggested: create a class MaskAnnotationIntrospector which extend from JacksonAnnotationIntrospector and override its findSerializer method, like this:

public class MaskAnnotationIntrospector extends JacksonAnnotationIntrospector {

    @Override
    public Object findSerializer(Annotated am) {
        Mask annotation = am.getAnnotation(Mask.class);
        if (annotation != null)
            return MaskingSerializer.class;

        return super.findSerializer(am);
    }
}

Therefore, you can have two instance of ObjectMapper. Add MaskAnnotationIntrospector to the one in which you want to Mask (e.g. for logging purpose):

mapper.setAnnotationIntrospector(new MaskAnnotationIntrospector());

The other instance which MaskAnnotationIntrospector has not set into it, do not mask any during serialization.

P.S. MaskAnnotationIntrospector can be extended from both JacksonAnnotationIntrospector & NopAnnotationIntrospector, but the latter does not provide any implementation for findSerializer method and calling super.findSerializer(am) simply return null and as a direct result, other Jackson annotation (such as @JsonIgnore) discarded, but by using the former, this problem solved

khesam109
  • 510
  • 9
  • 16
2

Instead of having MaskStringValueSerializer.java you can create module to bundle the serializer and register the module with objectmapper whenever you want , which will eventually allow you to have two different instances of objectmapper.

Create a module to bundle the serializer

public class MaskingModule extends SimpleModule {
    private static final String NAME = "CustomIntervalModule";
    private static final VersionUtil VERSION_UTIL = new VersionUtil() {};

    public MaskingModule() {
      super(NAME, VERSION_UTIL.version());
      addSerializer(MyBean.class, new MaskMapStringValueSerializer());
    }
}

Register the module with ObjectMapper and use it

 ObjectMapper objectMapper = new ObjectMapper().registerModule(new MaskingModule());
 System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(employee));

Also you can extend the Object Mapper , register the module and use it

public class CustomObjectMapper extends ObjectMapper {
    public CustomObjectMapper() {
      registerModule(new MaskingModule());
    }
  }


 CustomObjectMapper customObjectMapper = new CustomObjectMapper ();
 System.out.println(customObjectMapper .writerWithDefaultPrettyPrinter().writeValueAsString(employee));
Srinivasan Sekar
  • 2,049
  • 13
  • 22
  • Why create a subclass of `ObjectMapper`? Let the caller do the `registerModule()` call. – Andreas May 10 '19 at 04:24
  • added let him choose what he wants @Andreas – Srinivasan Sekar May 10 '19 at 04:34
  • @SrinivasanSekar thanks for your inputs. Your idea has a ring to it. I am also trying not to bind the Serializers to any specific bean type. What I am trying is to have a generic serializers which could mask any string value may it be either an attribute inside a type or a String value in a map or a whole type inside a type. – Samar May 10 '19 at 23:16
1

Remove the @JsonSerialize annotations, and put the logic of how to handle the @Mask annotation in a Module, e.g. have it add an AnnotationIntrospector.

You can now choose whether or not to call registerModule(Module module).

As for writing the module, I'll leave that up to you. If you have any questions about that, ask another Question.

Andreas
  • 154,647
  • 11
  • 152
  • 247
  • How would I do it for 2 different types using the same annotation? e.g in the above example I have annotation on both **`name`** and the **`categoryMap`** . Both of them would need 2 different serializers for the same annotation type. – Samar May 10 '19 at 23:08
  • Implement `findPropertyContentTypeResolver` and `findPropertyTypeResolver` – Andreas May 10 '19 at 23:32
-1

why don't you use two parameters one for original value and one for masked value. For example in this case you can use String name and String maskedName. then for logging you can use masked value.

Vimukthi
  • 846
  • 6
  • 19
  • Two parameters where? And how would it know which one to use? – Andreas May 10 '19 at 04:59
  • Add your mask annotation to maskedName field. when you are passing an object to logger it usually execute the toString method. override toString method and remove original name variable from it. then when logging it will print masked value only. for other things you can get original name value from object. don't mask the original value. – Vimukthi May 10 '19 at 05:42
  • Seems you missed the entire part of the question where OP is using `ObjectMapper` to generate (masked) JSON for the logging record, but is *also* using `ObjectMapper` to generate (unmasked) JSON for document indexing. The question is how to do *that*, i.e. how to make `ObjectMapper` only mask *sometimes*. – Andreas May 10 '19 at 16:14