Key Takeaways
- GDPR compliance is the most under-looked phenomenon, the failure of which is causing legal implications such as heavy monetary penalties.
- An application can be compliant to GDPR as early as in the development stage, making data privacy as a default and mandatory feature.
- The software libraries we use in our day-to-day development already have functionalities to make an application GDPR compliant without having to use paid, expensive tools.
- A very simple implementation using Spring Boot and AOP (Aspect Oriented Programming), explained in this article, will make you realize that GDPR compliance is not rRocket sScience.
- Masking, Encryption, Logging are the core of GDPR compliance, and they must be applied in all the artifacts that involve data privacy, such as PII (Personally Identifiable Information).
GDPR should be a default feature, added in every single application that handles user data, especially PII (Personally Identifiable Information).
Most organizations consider GDPR as luxury and have an impression that it needs special tools and experts to implement it.
Of course, knowledge of the entire GDPR specification is required, but once we are through the rules, we can see that the frameworks and design patterns we already use in our everyday development can very well be used to implement the GDPR rules.
Going forward, all applications should be GDPR compliant.
Features We Will Implement
When we talk about GDPR, the three important things we want to implement are:
- Encryption – Scrambling the actual data to hide information which needs a special mechanism to unscramble it.
- Masking - Hiding part of the information with a dummy pattern to make sure the information is retrieved but the person who reads is not authorised to view it fully.
- Logging – Recording every action that involves PII.
Even though these are three different features, the implementation can be simplified and concentrated by using simple programming patterns.
Techniques We Will Use
- Spring Boot framework – For a standard web application
- Java custom annotation technique – To annotate methods that needs GDPR treatment
- Aspect Oriented Programming (AOP) – Interception-based processing
- Simple Encryption and Masking Class – A mock Java class that scrambles, unscrambles and masks the data.
- Jackson Serializer – To process Java objects.
Good news. We don’t need any third-party libraries for this implementation.
Overview
[Click on the image to view full-size]
Spring Boot Application
Let us assume a simple use case:
- An API that accepts a User Object and returns a response.
- A service that saves the data to the database and returns saved data back to the Controller.
Note: We are not going to have a working database connection for this example, we will just assume that the service saves the data and returns the same to the user.
Entity
public class User {
private String name;
public User(){}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
Service
@Service
public class UserService {
private Logger logger = LoggerFactory.getLogger( "UserService" );
public User create( User user ){
logger.info( " Data inside create service " + user );
return user;
}
}
Controller
@RestController
public class UserController {
private Logger logger = LoggerFactory.getLogger( "UserController" );
@Autowired UserService userService;
@PostMapping("/plain")
public User plain(@RequestBody User user){
logger.info( " Data in the plain controller "+ user);
return userService.create(user);
}
}
Main Application (For the sake of completion)
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run( Application.class );
}
}
Custom Annotation
The second step is the custom annotation. Let us use standard Java to create it.
@Target( ElementType.METHOD)
@Retention( RetentionPolicy.RUNTIME)
public @interface GDPR { }
Let us break down this custom annotation:
- The
@interface
(instead of justinterface
) – makes this class available to be used as @GDPR in other classes. @Target(ElementType.METHOD)
– makes this annotation method specific (i.e., can be used only on methods not on classes or fields).@Retention(RetentionPolicy.RUNTIME)
– instructs the compiler to process this during runtime. The annotation need not be processed during compile time, so we use theRUNTIME
retention policy.
Now this interface can be used as @GDPR
in any desired method.
AOP Implementation
Spring Boot natively supports AOP. Using which, we will intercept any method that is annotated with the @GDPR
annotation.
Let us go ahead and create an interceptor class.
@Aspect
@Component
public class GDPRInterceptor {
private Logger logger = LoggerFactory.getLogger( GDPRInterceptor.class );
@Around("@annotation(GDPR)")
public Object encrypt(ProceedingJoinPoint joinPoint ) throws Throwable {
logger.info( "Method intercepted " );
}
@Component
– This Class is a standard Spring component.@Aspect
– This Class is a Spring AOP component and can intercept calls based on the pattern provided.@Around
("@annotation(GDPR)
") – This line has two parts:- a) The
@Around
helps to trigger these interceptors both before execution (when the method is called) and after execution (after processing is complete and before returning the final value). - b) The parameter ("
@annotation (GDPR)
") specifies which methods are intercepted. In our case, any method with the annotation@GDPR
, is intercepted.
- a) The
Controller with Annotation
Let us add a new method to our controller with the @GDPR
annotation. The modified controller should look like below:
@RestController
public class UserController {
private Logger logger = LoggerFactory.getLogger( "UserController" );
@Autowired UserService userService;
@PostMapping("/plain")
public User plain(@RequestBody User user){
logger.info( " Data in the plain controller "+ user);
return userService.create(user);
}
@PostMapping("/encrypt")
@GDPR
public User encrypt(@RequestBody User user){
logger.info( " Data in the encryption controller "+ user);
return userService.create(user);
}
}
Let us break down this controller:
The Controller/Web API has two methods with corresponding endpoints, /plain
and /encrypt
.
- Both APIs have the same return type and parameter type.
- Both the methods are calling the same service methods with the same data
- Only the encrypt method has the
@GDPR
annotation.
This way, the GDPR implementation does not disturb the existing business logic.
If you run the application as such and call the /plain
API, you should see log statements only from the UserController
and UserService
classes. But when you call the /encrypt
API, you should see an additional log statement from the GDPRInterceptor
class. It should be noted that the log statement from GDPRInterceptor
is printed first, because the controller is tapped before it calls the service, which is very critical in this implementation.
Encryption Service
Let’s create an EncryptionService
class. We are not going to implement an actual working encryption for now, instead, let us consider a simple mockup of an encryption.
@Component
public class EncryptionService {
private Object encrypt( Object data ){
if( data != null) {
data += " { ENCRYPTED } ";
}
return data;
}
private Object decrypt( Object data ){
String plainText = null;
if( data != null ) {
plainText = data.toString().replace(" { ENCRYPTED } ", "");
}
return plainText;
}
}
We have two methods:
encrypt
– Receives a single object, appends a string called{ENCRYPTED}
and returns the same.decrypt
– Receives a single object and removes the string{ENCRYPTED}
from it.
For Example, if the data is "MySampleData
," the encrypt method will return "MySampleData { ENCRYPTED }
," and if you pass this output to the decrypt method, you will get back the original data.
Customizing Object Mapper
The encrypt
and decrypt
methods we created are good for processing primitives. But a typical application would only deal with Java objects. It would be tedious to create individual transformers for every class type that an application deals with. For that purpose, we are going to extend the ObjectMapper
class provided by Jackson Library. We are going to alter the default object serialization method to include our encryption process.
First, we need to create two Custom Serializers, one for encryption and one for decryption.
Encryption Serializer
public class EncryptionSerializer extends JsonSerializer<Map> {
private Logger logger = LoggerFactory.getLogger( EncryptionSerializer.class );
EncryptionService encryptionService = new EncryptionService();
public EncryptionSerializer() {
super();
}
@Override
public void serialize(Map t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
t.forEach((k,v)->{
try {
jsonGenerator.writeObjectField( k.toString(),encryptionService.encrypt( v ));
} catch (IOException e) {
e.printStackTrace();
}
});
jsonGenerator.writeEndObject();
}
}
Decryption Serializer
public class DecryptionSerializer extends JsonSerializer<Map> {
EncryptionService encryptionService = new EncryptionService();
public DecryptionSerializer() {
super();
}
@Override
public void serialize(Map t, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeStartObject();
t.forEach((k,v)->{
try {
jsonGenerator.writeObjectField( k.toString(),encryptionService.decrypt( v ));
} catch (IOException e) {
e.printStackTrace();
}
});
jsonGenerator.writeEndObject();
}
}
The custom serializers extend JSONSerializer
and override the serialize
method. We are also casting this function with the Map
class, which will be explained later in this article.
Configuring Object Mappers
Let us build the custom object mappers that use these new serializers, thanks to Spring Boot configuration.
@Configuration
public class ApplicatonConfiguration {
@Bean
@Primary
public ObjectMapper objectMapper(){
return new ObjectMapper();
}
@Bean("encryptor")
public ObjectMapper encryptionMapper(){
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer( Map.class, new EncryptionSerializer());
mapper.registerModule( module );
return mapper;
}
@Bean("decryptor")
public ObjectMapper decryptionMapper(){
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addSerializer( Map.class, new DecryptionSerializer());
mapper.registerModule( module );
return mapper;
}
}
Here we have three types of ObjectMappers:
@Primary
– the default ObjectMapper
used by Spring.
@Bean("encryptor")
– ObjectMapper
, that will encrypt the Java Object (JSON).
@Bean("decryptor")
– ObjectMapper
, that will decrypt the Java Object (JSON).
Generalized the Jackson Mapper with java.util.Map
A typical application will have many types of Java classes/entities. Since we are going to reconfigure the Jackson serializer provided by Spring itself, we will not be able to include a Generic <?>
to our Serializer. It will also be tiresome to create a serializer for every class type. So we are going to generalize all Java class types as a Key Value object i.e., java.util.Map
. This generalization will help us serialize and deserialize any object, without worrying about actual data type.
Interceptor with Encryption
Consider the following scenario:
- The controller receives plain text data from the user through an API call.
- The controller calls the service method to save the user data.
- The service saves the data in encrypted format and returns the data back to the controller.
- The controller receives encrypted data from the service, but returns plain text back to the user as an API response.
To satisfy the above scenario, we need the data to be encrypted from the controller to the service and decrypted from the service to the controller.
Let us add this transformation code to our interceptor.
@Aspect
@Component
public class GDPRInterceptor {
private Logger logger = LoggerFactory.getLogger( GDPRInterceptor.class );
@Autowired ObjectMapper mapper;
@Autowired @Qualifier("encryptor") ObjectMapper encryptor;
@Autowired @Qualifier("decryptor") ObjectMapper decryptor;
@Around("@annotation(GDPR)")
public Object intercept(ProceedingJoinPoint joinPoint ) throws Throwable {
logger.info( " Method execution begins" );
Object[] params = joinPoint.getArgs();
Class<?> aClass = null;
Map<String,Object> intermediateData = null;
for( int index = 0 ; index < params.length ; index++ ){
aClass = params[index].getClass();
intermediateData = mapper.convertValue( params[index], new TypeReference<Map<String, Object>>() {}
);
params[index] = encryptor.convertValue(intermediateData, aClass);
}
Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
Object response = joinPoint.proceed(params);
intermediateData = mapper.convertValue(response, new TypeReference<Map<String, Object>>() {});
response = decryptor.convertValue( intermediateData, returnType );
logger.info ( " Method execution complete" );
return response;
}
}
The updated GDPRInterceptor
has the following changes:
- The reconfigured Object Mappers are included with the proper
@Qualifier
. - The method parameters are iterated, and every parameter is processed.
- The Parameter value is flattened using the standard Object Mapper to a
Map
Object. - The Map Object is then encrypted using the
encryptor
variant ofObjectMapper
and type casted to the actual parameter data type. - The Encrypted parameter is now forwarded to the Service.
- The Service saves the encrypted data and returns the same.
- The returned encrypted data is once again flattened to Map using the Standard Object Mapper.
- The flattened Map object is now decrypted using the
decryptor
variant ofObjectMapper
and type casted to the original return type.
Quick Testing
Plain Text API
Call
curl --location --request POST 'localhost:8080/plain' --header 'Content-Type: application/json' --data-raw '{"name”: "Developer"}'
Response
{"name": "Developer"}
Log Statements
INFO 9884 --- [nio-8080-exec-4] UserController : Data in the plain controller User{name='Developer'}
INFO 9884 --- [nio-8080-exec-4] UserService : Data inside create service User{name='Developer'}
Encrypted Text API
Call
curl --location --request POST 'localhost:8080/encrypt' --header 'Content-Type: application/json' --data-raw '{"name”: "Developer"}'
Response
{"name": "Developer"}
Log Statements
INFO 9068 --- [nio-8080-exec-2] GDPRInterceptor : Method execution begins
INFO 9068 --- [nio-8080-exec-2] UserController : Data in the encryption controller User{name='Developer { ENCRYPTED } '}
INFO 9068 --- [nio-8080-exec-2] UserService : Data inside create service User{name='Developer { ENCRYPTED } '}
INFO 9068 --- [nio-8080-exec-2] GDPRInterceptor : Method execution complete
As you can see, the API response is the same (original data) for both APIs. However, for the encrypt API, the log statements from controller and service show encrypted data.
Both controller methods are doing the same task, but just the @GDPR
implementation is doing the magic for us.
Masking
Masking sensitive data is an important aspect of GDPR compliance. Fortunately, it’s easy to include it in our current pattern.
Let us re-assume the scenario we discussed above.
The encryption stays the same, but when the API responds back to the user, the original data is masked. So, to satisfy this condition, we need to introduce a new method in EncryptionService
for Masking and modify the decrypt method to call this new masking function.
public Object mask( Object data ){
String maskedText = null;
String dataAsString = data.toString();
if( data != null )
maskedText = dataAsString.replace( dataAsString.substring( 1, 3 ), "XXX");
return maskedText;
}
public Object decrypt( Object data ){
String plainText = null;
if( data != null ) {
plainText = data.toString().replace(" { ENCRYPTED } ", "");
}
return mask(plainText);
}
Now if you call the /encrypt
API, the response will be:
{"name": "DXXXeloper"}
Logging
We already added logging to the interceptor. Any method that is annotated with @GDPR
are sensitive methods. Whenever these methods are called, the interceptor will log it around the execution.
Note - It is recommended not to log the actual data. Log files containing PII data are considered as a security risk.