Is Your Database Storing Plaintext? How Spring Boot Field‑Level Encryption Can Rescue You

The article explains why storing sensitive columns in plaintext leaves systems vulnerable even when the database is encrypted, and demonstrates a low‑intrusion, maintainable field‑level encryption solution for Spring Boot using JPA AttributeConverters, including code, pros, cons, and production best practices.

LuTiao Programming
LuTiao Programming
LuTiao Programming
Is Your Database Storing Plaintext? How Spring Boot Field‑Level Encryption Can Rescue You

Why Field‑Level Encryption Matters

Even when a database is encrypted at the storage or network level, a breach that exposes the database files still reveals any columns stored in plaintext. Attackers can directly read personal data from those columns.

Encryption Layers in Database Design

Full‑Database Encryption – e.g., Transparent Data Encryption (TDE) encrypts the entire database files.

Table‑Level Encryption – encrypts all columns of a table uniformly.

Field‑Level Encryption (FLE) – encrypts only selected sensitive columns.

Core Idea of Field‑Level Encryption

Only truly sensitive data is encrypted before being written to the database and automatically decrypted when read.

Typical Sensitive Fields

Personal Identifiable Information (PII): name, email, ID number, phone number.

Financial data: credit‑card number, bank account, payment identifier.

Medical & compliance data: patient ID, medical record index, private diagnosis information.

Implementation Options in Spring Boot

Option 1 – Manual encryption in the service layer (not recommended)

Intrusive to business logic.

Easy to forget.

High maintenance cost.

Option 2 – JPA @Converter (recommended)

Automatic encryption/decryption.

Transparent to business code.

Cleanest implementation.

Option 3 – Hibernate interceptor / listener

Suitable for complex global policies.

Steeper learning curve.

Usually overkill for most applications.

Chosen approach: JPA AttributeConverter

Step‑by‑Step Implementation

Project Structure

src/main/java/com/icoderoad/security
├── crypto
│   └── AESUtil.java
├── converter
│   └── EncryptionConverter.java
├── entity
│   └── Customer.java
├── repository
│   └── CustomerRepository.java
└── runner
    └── DemoRunner.java

AES Utility (simplified example)

package com.icoderoad.security.crypto;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class AESUtil {
    private static final String ALGORITHM = "AES";
    private static final String KEY = "MySecretKey12345"; // 16 chars = 128‑bit

    public static String encrypt(String data) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            return Base64.getEncoder().encodeToString(cipher.doFinal(data.getBytes()));
        } catch (Exception e) {
            throw new RuntimeException("Encrypt failed", e);
        }
    }

    public static String decrypt(String encrypted) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), ALGORITHM);
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            return new String(cipher.doFinal(Base64.getDecoder().decode(encrypted)));
        } catch (Exception e) {
            throw new RuntimeException("Decrypt failed", e);
        }
    }
}
Note: Hard‑coding the key is for demonstration only; production code should retrieve keys from a secure vault.

JPA AttributeConverter

package com.icoderoad.security.converter;

import com.icoderoad.security.crypto.AESUtil;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;

@Converter
public class EncryptionConverter implements AttributeConverter<String, String> {
    @Override
    public String convertToDatabaseColumn(String attribute) {
        return attribute == null ? null : AESUtil.encrypt(attribute);
    }

    @Override
    public String convertToEntityAttribute(String dbData) {
        return dbData == null ? null : AESUtil.decrypt(dbData);
    }
}

Entity with Selective Encryption

package com.icoderoad.security.entity;

import com.icoderoad.security.converter.EncryptionConverter;
import jakarta.persistence.*;

@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name; // not encrypted

    @Convert(converter = EncryptionConverter.class)
    private String ssn; // sensitive

    @Convert(converter = EncryptionConverter.class)
    private String creditCard; // sensitive

    // getters & setters omitted for brevity
}

Repository – No Special Handling Required

package com.icoderoad.security.repository;

import com.icoderoad.security.entity.Customer;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CustomerRepository extends JpaRepository<Customer, Long> { }

Demo Runner Showing Write‑as‑Ciphertext, Read‑as‑Plaintext

package com.icoderoad.security.runner;

import com.icoderoad.security.entity.Customer;
import com.icoderoad.security.repository.CustomerRepository;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
public class DemoRunner implements CommandLineRunner {
    private final CustomerRepository repo;

    public DemoRunner(CustomerRepository repo) {
        this.repo = repo;
    }

    @Override
    public void run(String... args) {
        Customer c = new Customer();
        c.setName("John Doe");
        c.setSsn("123-45-6789");
        c.setCreditCard("4111111111111111");
        repo.save(c);

        Customer dbCustomer = repo.findById(c.getId()).orElseThrow();
        System.out.println(dbCustomer.getSsn()); // prints decrypted value
    }
}

Actual Database Rows

id | name     | ssn           | credit_card
-----------------------------------------------
1  | John Doe | 7FhGhsd7a8=    | KJH78sdhJshd9=

When the database is exfiltrated, the attacker sees only Base64‑encoded ciphertext.

Pros and Cons

Pros

Protects only sensitive columns, keeping performance impact low.

Native JPA support yields clean, database‑agnostic code.

Cons

Encrypted columns cannot be used directly in WHERE clauses.

Key management introduces a new security surface.

Base64 encoding adds modest storage overhead.

Production Best Practices

Never hard‑code keys; use managed services such as AWS KMS, HashiCorp Vault, or Alibaba Cloud KMS.

Rotate encryption keys regularly.

Avoid encrypting columns that require frequent filtered queries.

Prefer AES‑256 over AES‑128 for stronger security.

Conclusion

Storing sensitive fields in plaintext is a design flaw. Field‑level encryption does not make a system invulnerable, but it raises the attack cost dramatically, especially for compliance‑heavy domains such as finance and healthcare. Adding an AttributeConverter‑based field‑level encryption layer to an existing Spring Boot + JPA project is a low‑effort, high‑impact improvement.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootDatabase SecurityAESjpaField-Level Encryption
LuTiao Programming
Written by

LuTiao Programming

LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.