Encrypt Everything: Building Secure REST APIs with Quarkus and Custom JSON Processing
Learn how to automatically encrypt and decrypt sensitive fields in your JSON payloads using Quarkus, JAX-RS providers, and AES-GCM encryption. All without cluttering your business logic.
Ever wanted to ensure that sensitive fields in your JSON payloads are encrypted by default, without rewriting your core business logic or cluttering your controllers? In this tutorial, you’ll build a Quarkus application that does exactly that.
You’ll learn how to:
Automatically decrypt incoming fields in HTTP POST requests.
Transparently encrypt fields in responses before they leave your service.
Implement this via JAX-RS message body providers that intercept and transform JSON data.
We’ll use AES-GCM encryption and build a dedicated crypto service, but the real value is in seeing how you can make encryption a reusable, invisible concern in your Java API.
Let’s start coding.
Set Up Your Project
First, generate a fresh Quarkus project with just the REST Jackson extensions. This example uses the Quarkus CLI. If you want to, you can go straight to the source code in my Github repository.
quarkus create app com.example:secure-service \
--extension=quarkus-rest-jackson
cd secure-service
This will give you a lean REST API base. All our encryption magic will be layered on top, using custom providers and a small crypto utility.
Write a CryptoService for Field-Level Encryption
You’ll need a utility to perform AES encryption and decryption. Create this service:
src/main/java/com/example/CryptoService.java
package com.example;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CryptoService {
// In production, store this securely!
private static final byte[] KEY = "your-32-byte-long-secret-key-!!!".getBytes(StandardCharsets.UTF_8);
private static final String ALGO = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12;
public String encrypt(String plaintext) throws Exception {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(KEY, "AES"), new GCMParameterSpec(128, iv));
byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
byte[] result = new byte[iv.length + encrypted.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(encrypted, 0, result, iv.length, encrypted.length);
return Base64.getEncoder().encodeToString(result);
}
public String decrypt(String base64) throws Exception {
byte[] input = Base64.getDecoder().decode(base64);
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(input, 0, iv, 0, iv.length);
Cipher cipher = Cipher.getInstance(ALGO);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(KEY, "AES"), new GCMParameterSpec(128, iv));
byte[] decrypted = cipher.doFinal(input, GCM_IV_LENGTH, input.length - GCM_IV_LENGTH);
return new String(decrypted, StandardCharsets.UTF_8);
}
}
This service uses AES in GCM mode, which is secure and authenticated. The IV (initialization vector) is randomly generated and prepended to the ciphertext. Just standard practice.
Create a Simple Message Model
Let’s define a basic data structure to hold the encrypted payload:
src/main/java/com/example/SecretMessage.java
package com.example;
public class SecretMessage {
public String message;
}
Build a REST Endpoint That Consumes Encrypted Messages
The controller will use the CryptoService
for testing, but won’t manually encrypt or decrypt the request/response. That’s handled by the providers you’ll build next. Rename the existing GreetingResource to:
src/main/java/com/example/MessageResource.java
package com.example;
import org.jboss.logging.Logger;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/messages")
public class MessageResource {
private static final Logger LOG = Logger.getLogger(MessageResource.class);
@Inject
CryptoService cryptoService;
@POST
@Path("/secret")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public SecretMessage handleSecret(SecretMessage message) {
LOG.infof("Received and decrypted message: '%s'", message.message);
return message;
}
@GET
@Path("/encrypt/{text}")
@Produces(MediaType.TEXT_PLAIN)
public String encryptForTesting(@PathParam("text") String text) throws Exception {
return cryptoService.encrypt(text);
}
}
Intercept and Decrypt Incoming Payloads
Here’s where the real trick comes in. By writing a MessageBodyReader
, we can intercept the incoming JSON and decrypt the message
field before the controller sees it.
src/main/java/com/example/EncryptedMessageBodyReader.java
package com.example;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.ext.MessageBodyReader;
import jakarta.ws.rs.ext.Provider;
@Provider
@Consumes(MediaType.APPLICATION_JSON)
public class EncryptedMessageBodyReader implements MessageBodyReader<SecretMessage> {
@Inject
CryptoService cryptoService;
@Inject
ObjectMapper objectMapper;
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type.equals(SecretMessage.class);
}
@Override
public SecretMessage readFrom(Class<SecretMessage> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException, WebApplicationException {
ObjectNode json = objectMapper.readValue(entityStream, ObjectNode.class);
try {
String encrypted = json.get("message").asText();
String decrypted = cryptoService.decrypt(encrypted);
SecretMessage result = new SecretMessage();
result.message = decrypted;
return result;
} catch (Exception e) {
throw new WebApplicationException("Failed to decrypt message", 400);
}
}
}
Encrypt the Outgoing Response Automatically (Optional)
You could also write a MessageBodyWriter<SecretMessage>
if you want to encrypt the response transparently. For this tutorial, the controller just returns the same SecretMessage
, which will already contain plaintext.
But you can extend this by creating:
public class EncryptedMessageBodyWriter implements MessageBodyWriter<SecretMessage> { ... }
And reverse the logic: take the outgoing plaintext message
, encrypt it, and write it into the response JSON.
Run and Test Your Encrypted Message API
Start the service in dev mode:
quarkus dev
In a separate terminal, encrypt a message using the helper endpoint:
curl http://localhost:8080/messages/encrypt/QuarkusIsAwesome
You’ll get back a Base64-encoded string like:
nD8ddB47YQZxBHQfVgFbdnUSFq...
Use this encrypted string as the request body:
curl -X POST http://localhost:8080/messages/secret \
-H "Content-Type: application/json" \
-d '{"message":"nD8ddB47YQZxBHQfVgFbdnUSFq..."}'
In your Quarkus console, you should see:
INFO Received and decrypted message: 'QuarkusIsAwesome'
The response body will return the same plaintext, or you can customize it to re-encrypt before responding.
Where to Go from Here
You now have an API that automatically decrypts incoming data and can encrypt responses without mixing cryptographic logic into your controller layer. This approach can be extended to:
Encrypt multiple fields via annotations (e.g.,
@Encrypted
)Use a more robust key management solution (like Vault)
Decrypt lists or nested objects
Integrate audit logging or redaction
This pattern is ideal for building secure services in regulated environments without adding friction to your development workflow.
If you want to go further, check out:
Keep your APIs clean, fast, and secure without drowning in boilerplate.