Building a Secure Diary with Quarkus, Bouncy Castle, and Panache
Protect your thoughts, one encrypted entry at a time.
In this hands-on tutorial, we’ll build a private, encrypted diary web application using Java and Quarkus. The application uses Bouncy Castle to encrypt your thoughts before persisting them in a PostgreSQL database with Panache ORM. We’ll also add a reactive web interface using Qute, making the app clean and usable without compromising on security.
This project is ideal for developers looking to understand strong encryption practices, Hibernate ORM with Panache, and how to wire it all together in a secure Quarkus application.
Project Setup
We’ll start by creating the Quarkus project with all the necessary extensions.
Create a new Quarkus project
You can generate the project using the Quarkus CLI or with Maven:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=secure-diary \
-DclassName="org.acme.diary.DiaryResource" \
-Dextensions="hibernate-orm-panache, jdbc-postgresql, qute, rest-jackson,quarkus-security"
cd secure-diary
Quarkus Dev Services will automatically spin up a PostgreSQL container during development. No Docker setup required.
Grab the full project from my Github repository. You can also find the Qute template with the css styling there.
Add Bouncy Castle to pom.xml
Inside your project, open the pom.xml
and add the following dependency:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
Create the CryptoService
To avoid cryptographic logic leaking into business code, we’ll encapsulate all encryption and decryption in a dedicated service.
application.properties
Add a static key for simplicity, but note that this is not secure for production:
# Base64-encoded 32-byte key (for AES-256)
encryption.secret=fLFzWniTwR2TJ4qf6sX+7hlfFePlxdnbwdT3+u5hN1U=
#Database configuration
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.database.generation=drop-and-create
#Setting the JCE security provider
quarkus.security.security-providers=BC
src/main/java/org/acme/diary/CryptoService.java
package org.acme.diary;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class CryptoService {
private static final String ENCRYPTION_ALGO = "AES/GCM/NoPadding";
private static final int IV_SIZE = 12; // recommended for GCM
private static final int TAG_LENGTH = 128;
private final SecretKeySpec key;
public CryptoService(@ConfigProperty(name = "encryption.secret") String base64Key) {
byte[] decodedKey = Base64.getDecoder().decode(base64Key.trim());
this.key = new SecretKeySpec(decodedKey, "AES");
}
public String encrypt(String plaintext) {
try {
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGO, "BC");
byte[] iv = new byte[IV_SIZE];
SecureRandom random = new SecureRandom();
random.nextBytes(iv);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
byte[] result = new byte[iv.length + ciphertext.length];
System.arraycopy(iv, 0, result, 0, iv.length);
System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);
return Base64.getEncoder().encodeToString(result);
} catch (Exception e) {
throw new IllegalStateException("Encryption failed", e);
}
}
public String decrypt(String base64Encrypted) {
try {
byte[] data = Base64.getDecoder().decode(base64Encrypted);
byte[] iv = Arrays.copyOfRange(data, 0, IV_SIZE);
byte[] ciphertext = Arrays.copyOfRange(data, IV_SIZE, data.length);
Cipher cipher = Cipher.getInstance(ENCRYPTION_ALGO, "BC");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new IllegalStateException("Decryption failed", e);
}
}
}
Define the DiaryEntry Entity
We’ll use Panache to define a simple entity for diary entries. Rename the MyEntity.java and insert below:
src/main/java/org/acme/diary/DiaryEntry.java
package org.acme.diary;
import java.time.LocalDate;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
@Entity
public class DiaryEntry extends PanacheEntity {
public LocalDate entryDate;
@Column(columnDefinition = "TEXT")
public String encryptedContent;
public boolean archived = false;
}
Create the REST + Templated UI Layer
This layer will serve both HTML views and RESTful JSON interactions.
src/main/java/org/acme/diary/DiaryResource.java
package org.acme.diary;
import java.time.LocalDate;
import java.util.List;
import io.quarkus.qute.Template;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam;
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.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path("/")
public class DiaryResource {
@Inject
CryptoService crypto;
@Inject
Template diary;
@GET
@Produces(MediaType.TEXT_HTML)
public String view(@QueryParam("date") LocalDate date) {
List<DiaryEntry> entries = date != null
? DiaryEntry.list("entryDate = ?1 and archived = false order by entryDate desc", date)
: DiaryEntry.list("archived = false order by entryDate desc");
entries.forEach(e -> e.encryptedContent = crypto.decrypt(e.encryptedContent));
return diary.data("entries", entries).render();
}
@POST
@Path("/entries")
@Transactional
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public void add(@FormParam("entryDate") LocalDate date, @FormParam("content") String content) {
var entry = new DiaryEntry();
entry.entryDate = date;
entry.encryptedContent = crypto.encrypt(content);
entry.persist();
}
@POST
@Path("/entries/{id}/archive")
@Transactional
public void archive(@PathParam("id") Long id) {
DiaryEntry entry = DiaryEntry.findById(id);
if (entry != null) {
entry.archived = true;
entry.persist();
}
}
}
Build the UI with Qute
Let’s create a single HTML page that lists entries and allows for adding and archiving them.
src/main/resources/templates/diary.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Secure Diary</title>
<style>
<!-- omitted for brevity -->
</style>
</head>
<body>
<div class="container">
<h1>My Secure Diary</h1>
<form action="/entries" method="post">
<label for="entryDate">Date:</label>
<input type="date" id="entryDate" name="entryDate" required>
<label for="content">Content:</label>
<textarea id="content" name="content" rows="5" placeholder="Write your diary entry here..." required></textarea>
<button type="submit">Save Entry</button>
</form>
<h2>Entries</h2>
<div class="filter-section">
<label for="dateFilter">Filter by date:</label>
<input type="date" id="dateFilter" onchange="filterByDate(this.value)">
</div>
<ul>
{#for entry in entries}
<li>
<strong>{entry.entryDate}</strong>
<pre>{entry.encryptedContent}</pre>
<form action="/entries/{entry.id}/archive" method="post" style="display:inline">
<button type="submit" class="archive-btn">Archive</button>
</form>
</li>
{/for}
</ul>
</div>
<script>
// Prefill the entry date with today's date
document.addEventListener('DOMContentLoaded', function() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('entryDate').value = today;
});
function filterByDate(date) {
window.location.href = '/?date=' + date;
}
</script>
</body>
Run It!
Start the app in dev mode:
./mvnw quarkus:dev
Visit http://localhost:8080 to view your secure diary.
Ideas to Expand
Add login/authentication using Quarkus OIDC or form-based auth.
Add tagging or mood analysis using LangChain4j.
Store encryption keys in Vault using Quarkus Vault extension.
Export diary entries as encrypted PDFs.
Add full-text search by indexing decrypted text at runtime only.
This diary app does more than store text: It models a secure-by-default mindset using tools Java developers already know. You're now in control of both your data and your encryption. Privacy isn’t just a feature, it’s a responsibility.