This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
Project Overview
ShareText is a Spring Boot application for securely sharing text through unique links.
- Create shareable text links
- Protect links with passwords
- Set expiration dates
- Use custom aliases
- Retrieve shared content through a web interface

The project stores shared content in MySQL and provides a frontend using HTML, CSS, and JavaScript.
Bug Fix or Performance Improvement
The original encryption implementation exposed its AES key directly in the frontend JavaScript:
const keyBytes = new Uint8Array([
11, 22, 33, 44, 55, 66, 77, 88,
99, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32
]);
This meant that anyone could inspect the website source code and recover the encryption key.
The application also used AES-CBC without authenticated integrity protection. This could allow encrypted data to be modified without reliable tamper detection.
The main security issues were:
- Encryption key exposed to every client
- Client-side encryption providing no real secret protection
- Shared content stored as plaintext in the database
- AES-CBC used without authentication
- No server-side key validation
- Passwords and content handled through the same exposed encryption model
Code
The main implementation changes are available in:
PayloadCrypto.javaSharedTextService.javaSharedTextController.javaapp.jsapplication.properties
PR: https://github.com/ajaym0urya/ShareText/pull/1
https://github.com/ajaym0urya/ShareText/commit/e41f95c05be0dc5a5ebd78f67c7aed7653b5dfec
My Improvements
I moved encryption responsibility from the browser to the server.
The frontend now sends normal JSON over HTTPS:
const payload = {
content: textContent.value,
expirationDate: calculateExpiry(expirationSelect.value),
password: sharePassword.value.trim() || null,
customAlias: customLinkAlias.value.trim() || null
};
The browser no longer contains the encryption key or encryption logic.
On the server, ShareText now uses AES-256-GCM:
private static final String CIPHER = "AES/GCM/NoPadding";
private static final int KEY_SIZE_BYTES = 32;
private static final int NONCE_SIZE_BYTES = 12;
A new random nonce is generated for every encryption operation. The stored value contains:
Base64(nonce + ciphertext + authentication tag)
AES-GCM provides both confidentiality and integrity. If someone modifies the encrypted database value, authentication fails during decryption.
Content is encrypted before persistence:
text.setContent(payloadCrypto.encrypt(request.getContent()));
Content is decrypted only after expiration and password checks succeed:
response.setContent(payloadCrypto.decrypt(text.getContent()));
Passwords continue to use BCrypt hashing:
text.setPasswordHash(
passwordEncoder.encode(request.getPassword())
);
The encryption key is now loaded from an environment variable:
sharetext.encryption-key=${SHARETEXT_ENCRYPTION_KEY}
For local testing, I generate a random 32-byte key in PowerShell:
$key = New-Object byte[] 32
$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
$rng.GetBytes($key)
$rng.Dispose()
$env:SHARETEXT_ENCRYPTION_KEY =
[Convert]::ToBase64String($key)
mvn spring-boot:run
This fix provides:
- HTTPS transport protection
- AES-256-GCM encryption at rest
- Random nonce generation
- Tamper detection
- Server-only encryption keys
- BCrypt password hashing
- No cryptographic secrets in frontend JavaScript
This is server-side encryption rather than strict zero-knowledge end-to-end encryption because the server decrypts content before returning it to an authorized user.
Thank you for reading my post.


Top comments (0)