java 57 lines · 10 steps

Transparent column encryption in Spring & JPA

A JPA AttributeConverter that AES-GCM encrypts String fields on the way into the database and decrypts them on the way out.

Explained by highlit
1@Component
2@Converter
3public class EncryptedStringConverter implements AttributeConverter<String, String> {
4 
5 private static final String ALGORITHM = "AES/GCM/NoPadding";
6 private static final int GCM_TAG_BITS = 128;
7 private static final int IV_LENGTH = 12;
8 
9 private final SecretKey secretKey;
10 private final SecureRandom secureRandom = new SecureRandom();
11 
12 public EncryptedStringConverter(@Value("${app.encryption.key}") String base64Key) {
13 byte[] keyBytes = Base64.getDecoder().decode(base64Key);
14 this.secretKey = new SecretKeySpec(keyBytes, "AES");
15 }
16 
17 @Override
18 public String convertToDatabaseColumn(String attribute) {
19 if (attribute == null) {
20 return null;
21 }
22 try {
23 byte[] iv = new byte[IV_LENGTH];
24 secureRandom.nextBytes(iv);
25 Cipher cipher = Cipher.getInstance(ALGORITHM);
26 cipher.init(Cipher.ENCRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_BITS, iv));
27 byte[] encrypted = cipher.doFinal(attribute.getBytes(StandardCharsets.UTF_8));
28 byte[] combined = ByteBuffer.allocate(iv.length + encrypted.length)
29 .put(iv)
30 .put(encrypted)
31 .array();
32 return Base64.getEncoder().encodeToString(combined);
33 } catch (GeneralSecurityException e) {
34 throw new IllegalStateException("Failed to encrypt column value", e);
35 }
36 }
37 
38 @Override
39 public String convertToEntityAttribute(String dbData) {
40 if (dbData == null) {
41 return null;
42 }
43 try {
44 byte[] combined = Base64.getDecoder().decode(dbData);
45 ByteBuffer buffer = ByteBuffer.wrap(combined);
46 byte[] iv = new byte[IV_LENGTH];
47 buffer.get(iv);
48 byte[] cipherText = new byte[buffer.remaining()];
49 buffer.get(cipherText);
50 Cipher cipher = Cipher.getInstance(ALGORITHM);
51 cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(GCM_TAG_BITS, iv));
52 return new String(cipher.doFinal(cipherText), StandardCharsets.UTF_8);
53 } catch (GeneralSecurityException e) {
54 throw new IllegalStateException("Failed to decrypt column value", e);
55 }
56 }
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1An AttributeConverter lets you intercept persistence at the field level so encryption stays invisible to your entity and query code.
  2. 2AES-GCM needs a fresh random IV per encryption, which you must store alongside the ciphertext to decrypt later.
  3. 3Prepending the IV to the ciphertext gives you a single self-contained blob that carries everything decryption requires.

Related explainers

typescript
import { registerLocaleData } from '@angular/common';
import localeFr from '@angular/common/locales/fr';
import localeFrExtra from '@angular/common/locales/extra/fr';
import localeDe from '@angular/common/locales/de';

Locale-aware bootstrapping in Angular

i18n localization dependency-injection
Intermediate 8 steps
typescript
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import * as Joi from 'joi';
 

Validating env config at boot in NestJS

configuration schema-validation environment-variables
Intermediate 8 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import { Cache } from 'cache-manager';
import { InjectRepository } from '@nestjs/typeorm';

A cache-aside country lookup in NestJS

cache-aside dependency-injection batch-lookup
Intermediate 8 steps
java
package com.acme.billing.config;
 
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;

Feature-flagged beans with Spring @ConditionalOnProperty

feature-flags conditional-beans strategy-pattern
Intermediate 5 steps
typescript
import { Injectable, effect, signal, computed } from '@angular/core';
 
interface Preferences {
  theme: 'light' | 'dark';

A signal-based preferences store in Angular

signals state-management persistence
Intermediate 7 steps
java
public static Map<String, String> parseCookieHeader(String header) {
    Map<String, String> cookies = new LinkedHashMap<>();
    if (header == null || header.isBlank()) {
        return cookies;

Parsing an HTTP Cookie header in Java

string-parsing http url-decoding
Intermediate 6 steps

Share this explainer

Here's the card — post it anywhere.

Transparent column encryption in Spring & JPA — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code