Mission: Impossible Encryption with Quarkus: Build a Secure PGP Message System in Java
A hands-on spy-themed tutorial using Quarkus, BouncyCastle, and real PGP encryption.
You wake up in a dimly lit briefing room.
A voice crackles through hidden speakers:
“New recruit, welcome to S.H.I.E.L.D.
Secure Hypertext Intelligence Encryption & Layered Defense.
Your first mission: build an end-to-end encrypted communication system using Quarkus and BouncyCastle.
Your second mission: survive.”
A dossier slides under the door. You open it.
Inside you see:
A Java 21 cheat sheet
A QR code linking to Quarkus
A crumpled sticky note reading:
“Don’t store private keys. Like ever.”
Congratulations.
Your career as a cryptographic field agent begins now.
In this tutorial, you will build a Quarkus-based spy communication service where:
Agents enroll and receive a PGP key pair.
The server stores only the public key.
Anyone can encrypt a message for an agent using that public key.
Only the agent, carrying their own private key + passphrase, can decrypt it.
This is real PGP.
Real encryption.
Real mistakes possible.
And real fun.
Gadget Setup: Prerequisites & Project Bootstrap
Prerequisites
You’ll need:
Java 21 (or 17+, but 21 is a good default for new Quarkus apps).
Maven 3.9+.
Recent Quarkus 3.x
curlor HTTP client of your choice.
Create the Quarkus project
From an empty directory:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme.spy \
-DprojectArtifactId=mission-impossible-encryption \
-DclassName=org.acme.spy.SpyMessageResource \
-Dpath=/secret-message \
-Dextensions="rest-jackson,quarkus-security"
cd mission-impossible-encryptionQuarkus will create a REST endpoint skeleton for you.
And if you don’t feel like doing this tutorial yourself, feel free to grab it from my Github repository!
Add BouncyCastle
Open pom.xml and add BouncyCastle dependencies in <dependencies>:
<!-- BouncyCastle provider (JCA/JCE + low-level crypto APIs) -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.82</version>
</dependency>
<!-- BouncyCastle OpenPGP APIs -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk18on</artifactId>
<version>1.82</version>
</dependency>Fun fact:
BouncyCastle contains zero bouncy castles.
Becoming an Agent - Creating Key Pairs and Secret Identities
Every spy needs:
A codename
An email
A public key
A private key
A passphrase
The ability to forget none of the above
But now with our new security model, the server will:
Generate the keys
Return both (once)
Store only the public key
Lose your private key?
That’s it. Messages are lost. Not even Q can save you.
Register the BouncyCastle provider
Add the following configuration to src/main/resources/application.properties:
quarkus.security.security-providers=BCThis ensures “BC” is available before any crypto operations.
Data classes: Agent and Public Keys
Create src/main/java/org/acme/spy/Agent.java:
package org.acme.spy;
public class Agent {
private String codeName;
private String email;
private String publicKeyArmored;
public Agent() {
}
public Agent(String codeName, String email, String publicKeyArmored) {
this.codeName = codeName;
this.email = email;
this.publicKeyArmored = publicKeyArmored;
}
public String getCodeName() {
return codeName;
}
public void setCodeName(String codeName) {
this.codeName = codeName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPublicKeyArmored() {
return publicKeyArmored;
}
public void setPublicKeyArmored(String publicKeyArmored) {
this.publicKeyArmored = publicKeyArmored;
}
}We’ll keep agents in-memory for this tutorial.
Response Type for Enrollment
Create src/main/java/org/acme/spy/AgentEnrollmentResponse.java:
package org.acme.spy;
public class AgentEnrollmentResponse {
public String codeName;
public String email;
public String publicKeyArmored;
public String privateKeyArmored;
public AgentEnrollmentResponse() {
}
public AgentEnrollmentResponse(String codeName,
String email,
String publicKeyArmored,
String privateKeyArmored) {
this.codeName = codeName;
this.email = email;
this.publicKeyArmored = publicKeyArmored;
this.privateKeyArmored = privateKeyArmored;
}
}The server will forget the private key immediately afterward.
Burn after reading.
Key generation service
Now the heart of the gadget.
Create src/main/java/org/acme/spy/SecretAgentKeyService.java:
package org.acme.spy;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.Date;
import org.bouncycastle.bcpg.ArmoredOutputStream;
import org.bouncycastle.bcpg.HashAlgorithmTags;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair;
import org.bouncycastle.openpgp.PGPKeyRingGenerator;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSignature;
import org.bouncycastle.openpgp.operator.PGPDigestCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyEncryptorBuilder;
import jakarta.enterprise.context.ApplicationScoped;
/**
* Service for generating PGP key pairs for secret agents.
*
* This class generates RSA 4096-bit key pairs and converts them to PGP format,
* producing both public and private key rings. The private key is encrypted
* with AES-256 using a passphrase, and both keys are returned in ASCII-armored
* (base64) format suitable for storage or transmission.
*/
@ApplicationScoped
public class SecretAgentKeyService {
/**
* Generates PGP key pairs for an agent with the given code name and email.
* Returns both public and private keys in ASCII-armored format.
*/
public AgentEnrollmentResponse generateAgentKeys(String codeName, String email, char[] passphrase) {
try {
String identity = codeName + “ <” + email + “>”;
// Create the PGP key ring generator with RSA keys and encryption settings
PGPKeyRingGenerator keyRingGen = createKeyRingGenerator(identity, passphrase);
// Generate separate public and private key rings
PGPPublicKeyRing publicKeyRing = keyRingGen.generatePublicKeyRing();
PGPSecretKeyRing secretKeyRing = keyRingGen.generateSecretKeyRing();
// Convert binary key data to ASCII-armored (base64) format
String publicKeyArmored = armor(publicKeyRing.getEncoded());
String privateKeyArmored = armor(secretKeyRing.getEncoded());
return new AgentEnrollmentResponse(codeName, email, publicKeyArmored, privateKeyArmored);
} catch (Exception e) {
throw new IllegalStateException(”Failed to generate PGP keys for “ + codeName, e);
}
}
/**
* Creates a PGP key ring generator with RSA 4096-bit keys.
* The private key will be encrypted with AES-256 using the provided passphrase.
*/
private PGPKeyRingGenerator createKeyRingGenerator(String identity, char[] passphrase)
throws NoSuchAlgorithmException, NoSuchProviderException, PGPException {
// Generate RSA 4096-bit key pair using BouncyCastle provider
KeyPairGenerator kpg = KeyPairGenerator.getInstance(”RSA”, “BC”);
kpg.initialize(4096);
KeyPair kp = kpg.generateKeyPair();
// Configure SHA1 for key checksum (required by PGPKeyRingGenerator)
PGPDigestCalculator sha1Calc = new JcaPGPDigestCalculatorProviderBuilder()
.build()
.get(HashAlgorithmTags.SHA1);
// Configure SHA-256 for secret key encryption
PGPDigestCalculator sha256Calc = new JcaPGPDigestCalculatorProviderBuilder()
.build()
.get(HashAlgorithmTags.SHA256);
// Convert JCA key pair to PGP format (version 4)
PGPKeyPair pgpKeyPair = new JcaPGPKeyPair(4, PGPPublicKey.RSA_GENERAL, kp, new Date());
// Configure signer for key certification using SHA-256
JcaPGPContentSignerBuilder signerBuilder = new JcaPGPContentSignerBuilder(
pgpKeyPair.getPublicKey().getAlgorithm(),
HashAlgorithmTags.SHA256);
// Configure private key encryption with AES-256 and the provided passphrase
JcePBESecretKeyEncryptorBuilder encryptorBuilder = new JcePBESecretKeyEncryptorBuilder(
PGPEncryptedData.AES_256, sha256Calc)
.setProvider(”BC”);
// Create the key ring generator with all configured components
// Note: SHA1 is required for key checksum calculations (4th parameter)
PGPKeyRingGenerator keyRingGen = new PGPKeyRingGenerator(
PGPSignature.POSITIVE_CERTIFICATION,
pgpKeyPair,
identity,
sha1Calc,
null,
null,
signerBuilder,
encryptorBuilder.build(passphrase));
return keyRingGen;
}
/**
* Converts binary PGP key data to ASCII-armored format (base64 encoded text).
* This format is human-readable and safe for email or text file storage.
*/
private String armor(byte[] bytes) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (ArmoredOutputStream aos = new ArmoredOutputStream(baos)) {
aos.write(bytes);
}
return baos.toString(StandardCharsets.UTF_8);
}
}
Yes, this is more “Mount Doom forge” than “Hello, World”. That’s OpenPGP.
Generates PGP key pairs for agents (code name and email) using BouncyCastle.
Creates RSA 4096-bit key pairs and converts them to PGP format (version 4).
Produces public and private key rings with SHA-256 signing and AES-256 encryption for the private key.
Encrypts the private key with the provided passphrase using AES-256.
Returns ASCII-armored keys (base64 text) suitable for storage or transmission, wrapped in an AgentEnrollmentResponse object.
Agent Registry - The Server Knows Who You Are But Cannot Decrypt Your Secrets
We need a small “agency database”.
Create src/main/java/org/acme/spy/AgentRegistry.java:
package org.acme.spy;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class AgentRegistry {
private final Map<String, Agent> agents = new ConcurrentHashMap<>();
public Agent enroll(Agent agent) {
agents.put(agent.getCodeName(), agent);
return agent;
}
public Agent find(String codeName) {
return agents.get(codeName);
}
public Collection<Agent> all() {
return agents.values();
}
}We store public keys only.
Like a safe public phone book… for spies.
Turning Cleartext Into Mission-Grade Ciphertext
Now the actual encryption/decryption using the agent’s key material.
Create src/main/java/org/acme/spy/PgpCryptoService.java:
package org.acme.spy;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Iterator;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPMarker;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.openpgp.operator.jcajce.JcaKeyFingerprintCalculator;
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class PgpCryptoService {
public String encrypt(String plainText, String armoredPublicKey) {
try {
PGPPublicKey publicKey = readEncryptionKey(armoredPublicKey);
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (org.bouncycastle.bcpg.ArmoredOutputStream armoredOut = new org.bouncycastle.bcpg.ArmoredOutputStream(
out)) {
PGPEncryptedDataGenerator encGen = new PGPEncryptedDataGenerator(
new JcePGPDataEncryptorBuilder(PGPEncryptedData.AES_256)
.setWithIntegrityPacket(true)
.setSecureRandom(new java.security.SecureRandom())
.setProvider(”BC”));
encGen.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey)
.setProvider(”BC”));
OutputStream encOut = encGen.open(armoredOut, new byte[4096]);
PGPCompressedDataGenerator comData = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);
try (OutputStream compressedOut = comData.open(encOut)) {
PGPLiteralDataGenerator lGen = new PGPLiteralDataGenerator();
byte[] bytes = plainText.getBytes(StandardCharsets.UTF_8);
try (OutputStream literalOut = lGen.open(compressedOut,
PGPLiteralData.TEXT,
PGPLiteralData.CONSOLE,
bytes.length,
new Date())) {
literalOut.write(bytes);
}
}
encOut.close();
}
return out.toString(StandardCharsets.UTF_8);
} catch (Exception e) {
throw new IllegalStateException(”Failed to encrypt message”, e);
}
}
public String decrypt(String armoredCipherText,
String armoredPrivateKey,
char[] passphrase) {
try (InputStream in = PGPUtil.getDecoderStream(
new ByteArrayInputStream(
armoredCipherText.getBytes(StandardCharsets.UTF_8)))) {
PGPObjectFactory pgpFact = new PGPObjectFactory(in, new JcaKeyFingerprintCalculator());
Object obj = pgpFact.nextObject();
if (obj instanceof PGPEncryptedDataList encryptedDataList) {
return decryptEncryptedDataList(encryptedDataList, armoredPrivateKey, passphrase);
} else if (obj instanceof PGPMarker) {
obj = pgpFact.nextObject();
PGPEncryptedDataList encList = (PGPEncryptedDataList) obj;
return decryptEncryptedDataList(encList, armoredPrivateKey, passphrase);
} else {
throw new IllegalStateException(”Unexpected PGP object: “ + obj);
}
} catch (Exception e) {
throw new IllegalStateException(”Failed to decrypt message”, e);
}
}
private String decryptEncryptedDataList(PGPEncryptedDataList encList,
String armoredPrivateKey,
char[] passphrase) throws Exception {
PGPSecretKeyRingCollection secretKeyRings = readSecretKeyRings(armoredPrivateKey);
PGPPublicKeyEncryptedData encryptedData = null;
PGPPrivateKey privateKey = null;
Iterator<PGPEncryptedData> it = encList.getEncryptedDataObjects();
while (it.hasNext() && privateKey == null) {
PGPPublicKeyEncryptedData pked = (PGPPublicKeyEncryptedData) it.next();
PGPSecretKey secretKey = secretKeyRings.getSecretKey(pked.getKeyIdentifier().getKeyId());
if (secretKey != null) {
privateKey = secretKey.extractPrivateKey(
new JcePBESecretKeyDecryptorBuilder()
.setProvider(”BC”)
.build(passphrase));
encryptedData = pked;
}
}
if (privateKey == null || encryptedData == null) {
throw new IllegalStateException(”No suitable private key found for decryption”);
}
InputStream clear = encryptedData.getDataStream(
new JcePublicKeyDataDecryptorFactoryBuilder()
.setProvider(”BC”)
.build(privateKey));
PGPObjectFactory plainFact = new PGPObjectFactory(clear, new JcaKeyFingerprintCalculator());
Object message = plainFact.nextObject();
if (message instanceof PGPCompressedData compressedData) {
PGPObjectFactory innerFact = new PGPObjectFactory(
compressedData.getDataStream(),
new JcaKeyFingerprintCalculator());
message = innerFact.nextObject();
}
if (message instanceof PGPLiteralData literalData) {
InputStream literalStream = literalData.getInputStream();
ByteArrayOutputStream out = new ByteArrayOutputStream();
literalStream.transferTo(out);
return out.toString(StandardCharsets.UTF_8);
}
throw new IllegalStateException(”Unsupported PGP message type: “ + message);
}
private PGPPublicKey readEncryptionKey(String armoredPublicKey) throws Exception {
try (InputStream keyIn = PGPUtil.getDecoderStream(
new ByteArrayInputStream(
armoredPublicKey.getBytes(StandardCharsets.UTF_8)))) {
PGPObjectFactory pgpFact = new PGPObjectFactory(keyIn, new JcaKeyFingerprintCalculator());
Object obj = pgpFact.nextObject();
if (!(obj instanceof PGPPublicKeyRing)) {
throw new IllegalStateException(”Not a public key ring”);
}
PGPPublicKeyRing keyRing = (PGPPublicKeyRing) obj;
Iterator<PGPPublicKey> it = keyRing.getPublicKeys();
while (it.hasNext()) {
PGPPublicKey key = it.next();
if (key.isEncryptionKey()) {
return key;
}
}
}
throw new IllegalStateException(”No encryption key found in public key ring”);
}
private PGPSecretKeyRingCollection readSecretKeyRings(String armoredPrivateKey)
throws Exception {
try (InputStream keyIn = PGPUtil.getDecoderStream(
new ByteArrayInputStream(
armoredPrivateKey.getBytes(StandardCharsets.UTF_8)))) {
return new PGPSecretKeyRingCollection(
keyIn,
new JcaKeyFingerprintCalculator());
}
}
}Yes, it’s a lot. That’s why people wrap this in libraries. Here we keep it explicit for learning.
Field Operations: REST Endpoints
We now glue everything together with Quarkus REST endpoints.
We’ll implement:
POST /agent/enroll– Create new agent identity (keys included).POST /message/encrypt– Encrypt a message for a recipient.POST /message/decrypt– Decrypt a message (with passphrase).GET /agents/compromised– List agents marked as compromised.
Agent Enrollment Endpoint
Create src/main/java/org/acme/spy/AgentResource.java:
package org.acme.spy;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path(”/agent”)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class AgentResource {
@Inject
SecretAgentKeyService keyService;
@Inject
AgentRegistry registry;
public static class EnrollRequest {
public String codeName;
public String email;
public String passphrase;
}
@POST
@Path(”/enroll”)
public Response enroll(EnrollRequest req) {
AgentEnrollmentResponse keys = keyService.generateAgentKeys(
req.codeName,
req.email,
req.passphrase.toCharArray());
// Store public key only
registry.enroll(new Agent(req.codeName, req.email, keys.publicKeyArmored));
// Return full keypair only once
return Response.status(Response.Status.CREATED)
.entity(keys)
.build();
}
}
This is like issuing a spy their first gadget.
If they drop it down a drain, you shrug and walk away.
Encryption & Decryption Endpoints
Create src/main/java/org/acme/spy/EncryptRequest.java:
package org.acme.spy.dto;
public class EncryptRequest {
public String recipient;
public String plainText;
}Create src/main/java/org/acme/spy/DecryptRequest.java:
package org.acme.spy.dto;
public class DecryptRequest {
public String recipient;
public String cipherText;
public String privateKeyArmored;
public String passphrase;
}Create src/main/java/org/acme/spy/EncryptedMessage.java:
package org.acme.spy.dto;
public class EncryptedMessage {
public String recipient;
public String cipherText;
}Replace the generated SpyMessageResource with a full resource.
Edit src/main/java/org/acme/spy/SpyMessageResource.java:
package org.acme.spy;
import java.util.Map;
import org.acme.spy.dto.DecryptRequest;
import org.acme.spy.dto.EncryptRequest;
import org.acme.spy.dto.EncryptedMessage;
import jakarta.inject.Inject;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
@Path(”/message”)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class SpyMessageResource {
@Inject
AgentRegistry registry;
@Inject
PgpCryptoService crypto;
@POST
@Path(”/encrypt”)
public Response encrypt(EncryptRequest req) {
Agent recipient = registry.find(req.recipient);
if (recipient == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(”Unknown agent: “ + req.recipient)
.build();
}
String cipher = crypto.encrypt(
req.plainText,
recipient.getPublicKeyArmored());
EncryptedMessage msg = new EncryptedMessage();
msg.recipient = req.recipient;
msg.cipherText = cipher;
return Response.ok(msg).build();
}
@POST
@Path(”/decrypt”)
public Response decrypt(DecryptRequest req) {
if (req.privateKeyArmored == null || req.privateKeyArmored.isBlank()) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(”Missing private key.”)
.build();
}
try {
String plain = crypto.decrypt(
req.cipherText,
req.privateKeyArmored,
req.passphrase.toCharArray());
return Response.ok(
Map.of(
“status”, “DECRYPTED”,
“message”, plain,
“coolnessLevel”, “Jason Bourne”,
“martiniCount”, 3))
.build();
} catch (Exception e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(”Decryption failed: “ + e.getMessage())
.build();
}
}
}Quarkus Dev Mode: Test Your Gadget Live
Start dev mode:
./mvnw quarkus:devQuarkus starts up and keeps watching your files. Every change recompiles on the fly.
Your spy server is live.
Field Test: Encrypting & Decrypting Over HTTP
Let’s walk through an end-to-end mission using curl.
Enroll an agent
curl -X POST http://localhost:8080/agent/enroll \
-H "Content-Type: application/json" \
-d '{
"codeName": "BLACKWIDOW",
"email": "natasha@shield.gov",
"passphrase": "super-secret-passphrase"
}'Response includes:
Public key
Private key
Show it ONCE and never again
The server stores ONLY the public key
For example:
{
"codeName": "BLACKWIDOW",
"email": "natasha@shield.gov",
"publicKeyArmored": "-----BEGIN PGP PUBLIC KEY BLOCK-----\n...",
"privateKeyArmored": "-----BEGIN PGP PRIVATE KEY BLOCK-----\n...",
"compromised": false
}
Copy the private key block into a local file e.g.
blackwidow-private.asc
Treat it like plutonium.
Encrypt a message
curl -X POST http://localhost:8080/message/encrypt \
-H "Content-Type: application/json" \
-d '{
"recipient": "BLACKWIDOW",
"plainText": "The pizza is in the oven"
}'Expected output:
{
“recipient”: “BLACKWIDOW”,
“cipherText”: “-----BEGIN PGP MESSAGE-----\n...”
}You now have an armored PGP message.
Decrypt a Message
Copy the value of cipherText and the privateKeyArmored use it here:
PRIVATE_KEY=$(cat blackwidow-private.asc)
curl -X POST http://localhost:8080/message/decrypt \
-H "Content-Type: application/json" \
-d "{
\"recipient\":\”BLACKWIDOW\",
\"cipherText\":\”-----BEGIN PGP MESSAGE-----\n...\",
\"privateKeyArmored\":\”$PRIVATE_KEY\",
\"passphrase\":\"super-secret-passphrase\"
}"Expected output:
{
“status”: “DECRYPTED”,
“message”: “The pizza is in the oven”,
“coolnessLevel”: “Jason Bourne”,
“martiniCount”: 3
}Security Briefing: Some Serious Points
Drop the Humor. Put on the Serious Face.
Crypto is Hard. No — Crypto is Brutally Hard.
Security is not “annoying” hard.
It is pitfall-ridden, mathematically unforgiving, quantum-threatened, career-ending hard.
Here is the reality:
RSA 4096 + AES-256 is strong today
But crypto ages, and quantum computing is already sharpening its knives.
Post-quantum algorithms (PQC) are being standardized.
Future Java releases are preparing for hybrid and PQC-compatible primitives.Never store private keys on the server
You didn’t. Good agent.Private keys belong only to field agents
Your server is a public key directory — not a vault.Passphrases must not be transmitted in plaintext
In production:
always HTTPS, consider client-side decryption, zero logging of crypto material.A breach of the server must not expose old messages
Our design achieves this: the server never had the private key.Crypto cannot be bolted on
It must be part of architecture, threat modeling, and lifecycle planning.
If this section scared you a bit, good.
Only scared developers build secure systems.
Where To Take This Next
Ways to level up this tutorial:
Replace in-memory registry with a database (Panache + PostgreSQL).
Store only public keys in your app; move private keys to a secure vault.
Add signing / verification so agents can detect tampering.
Integrate with your existing identity system (Keycloak, JWT).
Wrap key handling into a reusable Quarkus extension or library.
You’ve just built a functioning PGP encryption service on Quarkus.
Your gadget is live.



