March 29, 2024

The ContactSunny Blog

Tech from one dev to another

Encrypting and Decrypting data in MongoDB with a SpringBoot project

8 min read

In quite a few applications, we’ll have a requirement to keep the data in our databases encrypted so that even if somebody gets into the database, they might not understand what the data is. Encrypting is crucial in many applications. With the rise of NoSQL databases these days, we’ll take a look at how we can encrypt data going into a MongoDB database from our Spring Boot application. We’ll also see how we can decrypt that data after getting it from the database into our application.

One thing you need to know before trying this on any production-grade application is that this will slow things down. There are two extra steps involved in this process – encrypting and decrypting the data. This will slow down your application a bit, and depending on the amount of database reads and writes you have, this could have a significant impact on the performance of your application. Keep this in mind before you decide to encrypt everything in your database.


The Process

We want to encrypt all data being saved to the database, and all data coming out from the database. So we’ll write an encrypt() and a decrypt() method, obviously. But we don’t want to call these methods in our code every time we save and retrieve from the database. That’s too much code duplication. We want these methods to be called automatically every time we save something to MongoDB and every time we fetch something from MongoDB.

For this, we’ll use a couple of event listeners that MongoDB provides out of the box. Specifically, we’ll be extending the AbstractMongoEventListener class and creating two new classes – MongoDBBeforeSaveEventListener and MongoDBAfterLoadEventListener. As the name suggests, the former will be called every time we save something to MongoDB and the latter will be called every time we load something from MongoDB. We’ll be encrypting and decrypting our data in these two classes. It’s easier than it sounds.


The Encryption

Before we can get to the event listeners, we’ll have to write up our encryption and decryption methods. For this example, I’m keeping this super simple and using the BasicTextEncryptor class which comes bundled in the Java Util package. Depending on the application you’re working on, you can have custom implementation of encryption and make it as complicated as you please. But do keep in mind, the complex the encryption, the slower the application’s response becomes.

Anyway, the whole encryption utility for this example project is as follows:

@Component
public class EncryptionUtil {

    private BasicTextEncryptor textEncryptor = null;

    public EncryptionUtil() {

        textEncryptor = new BasicTextEncryptor();
        textEncryptor.setPassword("some-random-password");
    }

    public String encrypt(String textToEncrypt) {
        return this.textEncryptor.encrypt(textToEncrypt);
    }

    public String decrypt(String encryptedText) {
        return this.textEncryptor.decrypt(encryptedText);
    }
}

We have annotated the class with the @Component annotation. This way, we can autowire the class in our other classes and an instance of this class will be automatically injected into that class, we don’t have to manually create an object. It makes this easier, you’ll see.

The two methods – encrypt() and decrypt() – will do exactly what you think they do. It’s just one line of code using the built-in basic encryptor. We’ll see this in action in the following sections.


The MongoDBBeforeSaveEventListener event listener

The first event listener we’ll be looking at is the MongoDBBeforeSaveEventListener. We’ll extend the AbstractMongoEventListener class which is part of the MongoDB library provided by Spring framework. We get access to quite a few methods and events in this class.

For this particular application, where we want this method to be called every time, just before we save an object to the database, we’ll be overriding the onBeforeSave() method. The argument for this method is a BeforeSaveEvent<Object> object, which, along with some event related information, will have the document which is being saved in the database.

In this method, we’ll take the document from the argument, and call our encryption method to encrypt all the fields that we want encrypted. We’ll maintain a list of fields which we don’t want to be encrypted. In this example, I’ll not be encrypting the _id and the _class fields. If you’re okay with having encrypted ID fields as references in other documents in your database, you could encrypt the _id field as well. That’s a decision you have to make.

Once we’re done with the encryption, we’ll call the super.onBeforeSave() method and pass the same event object that we got as argument. The only difference is, this event object now has the encrypted document. Next, the document follows the rest of the life cycle without any change and is saved in the database. The code snippet for this class is as follows:

public class MongoDBBeforeSaveEventListener extends AbstractMongoEventListener<Object> {

    private static Logger logger = LogManager.getLogger(MongoDBBeforeSaveEventListener.class);

    @Autowired
    private EncryptionUtil encryptionUtil;

    @Override
    public void onBeforeSave(BeforeSaveEvent<Object> event) {

        Document eventObject = event.getDocument();

        /*
        We'll skip these because:
        * We don't want to encrypt the ID as we'll be querying on this most of the times in our business logic.
        * The '_class' field is a meta-data field added by SpringBoot which really is not very important in terms of
            being encrypted or not.
         */
        List<String> keysNotToEncrypt = Arrays.asList("_class", "_id");

        for ( String key : eventObject.keySet() ) {
            if (!keysNotToEncrypt.contains(key)) {
                eventObject.put(key, this.encryptionUtil.encrypt(eventObject.get(key).toString()));
            }
        }

        logger.info("DB Object: " + new Gson().toJson(eventObject));

        super.onBeforeSave(event);
    }
}

As you can see from the code snippet above, we have a list of fields in the keysNotToEncrypt variable, any field we don’t want to be encrypted, we can add it to this list and it’ll be saved to the database as is. Let’s now look at the decryption magic.


The MongoDBAfterLoadEventListener event listener

This is the class where the decryption is happening. Whenever we fetch an object from the database, the “after load” event listener is invoked. Similar to the previous class we talked about, we get the onAfterLoad() method here which we’ll be overriding. Also, the method gets an object of the class AfterLoadEvent<Object> as the argument. Among other things, this object will have the document that was fetched, or “loaded,” from the database.

We again maintain a list of fields which we don’t want to decrypt. Apart from those fields, we’ll decrypt all other fields, and then load that decrypted document back to the event object, we’ll then send this object to the super class. The flow continues from there as usual without any changes. The document will eventually reach the entity class, where an instance of the class will be created and these decrypted values will be loaded into it. Let’s now look at the event listener class.

public class MongoDBAfterLoadEventListener extends AbstractMongoEventListener<Object> {

    private static Logger logger = LogManager.getLogger(MongoDBAfterLoadEventListener.class);

    @Autowired
    private EncryptionUtil encryptionUtil;

    @Override
    public void onAfterLoad(AfterLoadEvent<Object> event) {

        Document eventObject = event.getDocument();

        /*
        We'll skip these because:
        * We don't want to encrypt the ID as we'll be querying on this most of the times in our business logic.
        * The '_class' field is a meta-data field added by SpringBoot which really is not very important in terms of
            being encrypted or not.
         */
        List<String> keysNotToDecrypt = Arrays.asList("_class", "_id");

        for ( String key : eventObject.keySet() ) {
            if (!keysNotToDecrypt.contains(key)) {
                eventObject.put(key, this.encryptionUtil.decrypt(eventObject.get(key).toString()));
            }
        }

        logger.info("DB Object: " + new Gson().toJson(eventObject));

        super.onAfterLoad(event);
    }
}

Pretty similar to the “before save” event listener, the only difference, we’re decrypting the data instead of encrypting it. Let’s now see the entity class that we’re using for this example.


The Entity class

I went way over my creative levels to name this class, I call it, Sample.java. I know, you couldn’t have thought of a spectacular name like this by your own. So yes, you can reuse this name. Anyway, I’ve kept the class super simple. Below is the whole class:

@Document(collection = "samples")
public class Sample {

    @Id
    private String id;
    private String name;
    private String description;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

The repository for the entity class

We have to define a repository class for the entity class Sample.java we created earlier. We need this so that we can easily interact with the database without writing any boiler plate code. It’s a simple, empty class that we need to create. The class is as follows:

public interface SampleRepository extends MongoRepository<Sample, String> {
}

Bringing it all together

Now, we’ll see how we can bring it all together in our main class and check if our code is working. First, we have to register the two event listeners we have defined. For this, we’ll use the @Bean annotation available in Spring. This way, the system will know where to find these two classes when required. The code snippet is as follows:

@Bean
public MongoDBBeforeSaveEventListener mongoDBBeforeSaveEventListener() {
    return new MongoDBBeforeSaveEventListener();
}

@Bean
public MongoDBAfterLoadEventListener mongoDBAfterLoadEventListener() {
    return new MongoDBAfterLoadEventListener();
}

Next, we’ll @Autowire the repository class that we created in the previous step:

@Autowired
private SampleRepository sampleRepository;

All that’s left now is to create an object and save it to the database. We can do that like this:

long timeStamp = System.currentTimeMillis();

Sample sample = new Sample();
sample.setName("Test name - " + timeStamp);
sample.setDescription("Test description - " + timeStamp);

sample = sampleRepository.save(sample);

logger.info("ID of saved object: " + sample.getId());

As you can see, we are using the repository to save the object to the database. This object will have two fields encrypted in the database – name and description. When we fetch this object from the database, it should be returned to us decrypted. Let’s check that as well:

Optional<Sample> fetchedObject = sampleRepository.findById(sample.getId());

logger.info("Fetched object: " + new Gson().toJson(fetchedObject.get()));

For this, we should get an output similar to the following:

Fetched object: {"id":"5e158452c009e93dc3237f93","name":"Test name - 1578468434188","description":"Test description - 1578468434188"}

You’ll not get the same values that you see here, because of the timestamp element in the values. Except that, everything else should be the same. But how to make sure that the values are actually encrypted? Well, just check the database. Below is a screenshot of the database showing the exact same document:

encrypted document in mongodb

Also, if you check the two event listeners, we have logs in place to print the document being saved and being fetched. So we can find those logs in the console and check the values:

DB Object before save: {"name":"J3hv48Rt3cm4R8Obkk5m4ujbNrBIwAHQ+M45D2C7oQqrCMk5XxBvVA\u003d\u003d","description":"/BlKNRnUoNBpsPxVCgbsaaXueNfvFSrr/oGhFb/w1ouDX1XGNsIZy+m6LHABipe/","_class":"com.contactsunny.poc.MongoDBSpringBootEncryptionDecryptionPOC.models.Sample"}
DB Object after load: {"_id":{"timestamp":1578468434,"machineIdentifier":12585449,"processIdentifier":15811,"counter":2326419},"name":"Test name - 1578468434188","description":"Test description - 1578468434188","_class":"com.contactsunny.poc.MongoDBSpringBootEncryptionDecryptionPOC.models.Sample"}

As you can see, the encryption and decryption magic is working.


If you’re interested in getting straight to the code, you can fork the complete project from my Github repo. And if you have any doubt or suggestions for this, please feel free to leave comments below.

5 thoughts on “Encrypting and Decrypting data in MongoDB with a SpringBoot project

  1. I’m getting below errors, please check and respond.

    Class:
    MongoDBAfterLoadEventListener

    Line # 24:
    Document eventObject = event.getDocument();

    Error:
    The method getDocument() is undefined for the type AfterLoadEvent

    Class:
    MongoDBBeforeSaveEventListener

    Line # 24:
    Document eventObject = event.getDocument();

    Error:
    The method getDocument() is undefined for the type BeforeSaveEvent

  2. Hi Ahmad,

    This could be a version issue. Make sure you’re using a version of spring-boot-starter-parent which has the document object in the event object. Older versions of the artifact didn’t have this. As far as I can tell from the info you’ve provided, that could be the issue.

    Thanks!

    1. The encryption works perfectly with my application but AfterLoad the encrypted data does not get decrypted. I follow the steps exactly. Do you please have any advice.

  3. Hi Sunny,

    How can we achieve encryption, decryption for complex objects like one Employee may have List how can we achieve this. Please suggest.
    Thanks!

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.