Why We Added RSA + AES-GCM Hybrid Encryption to Our WMS
Flash Warehouse WMS has no JWT or session mechanism on the backend. We implemented RSA 2048-bit key exchange with AES-256-GCM content encryption, triggered by the X-Encryption-Enabled header and processed transparently in a Spring Boot filter chain. This post covers the technical decision, architecture, and implementation details.
Background: A Backend Without JWT
The Flash Warehouse WMS Spring Boot backend was designed with a lightweight authentication approach: multi-tenant data isolation via a binding_user_id field on every database table, with no JWT tokens or server-side sessions. This simplifies backend state management, but it creates a problem -- sensitive data in request bodies (user passwords, product prices, financial figures) travels across the network in plaintext.
Even with HTTPS deployed, relying solely on transport-layer encryption is not sufficient. Reverse proxies, logging systems, and debugging tools can all access plaintext data after TLS termination. For a warehouse system that handles 16 distinct bill types -- purchase orders, sales shipments, stock transfers, financial settlements -- application-layer encryption is a necessary layer of defense in depth.
The Technical Decision: Why Hybrid Encryption
Pure RSA encryption has a hard limit: an RSA 2048-bit key can encrypt at most 245 bytes of data. A single purchase input bill with dozens of line items easily exceeds several kilobytes when serialized to JSON, far beyond what RSA can handle directly.
We adopted the industry-standard hybrid encryption approach:
- AES-256-GCM encrypts the actual request body with no size limit, and GCM mode provides both encryption and integrity verification in a single operation
- RSA 2048-bit encrypts only the AES key itself, with a fresh random key generated for every request
The security property is straightforward: even if an attacker compromises one request's AES key, it cannot decrypt any other request, because every key is independently and randomly generated.
Frontend Encryption Flow
The PC frontend (Vue 3) uses the JSEncrypt library and the browser's native Web Crypto API. The core logic lives in src/utils/rsa.js inside the encryptHybrid function:
// 1. Generate a random AES-256 key and 12-byte IV
const key = await crypto.subtle.generateKey(
{ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
);
const iv = crypto.getRandomValues(new Uint8Array(12));
// 2. Encrypt the plaintext with AES-GCM
const ct = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, dataBuf);
// 3. Encrypt the AES key with the RSA public key
const enc = new JSEncrypt();
enc.setPublicKey(PUBLIC_KEY);
const ek = enc.encrypt(keyB64);
// 4. Return the encryption envelope
return { alg: 'RSA+AES-GCM', ek, iv: ivB64, ct: ctB64 };
When sending the request, the frontend must also set the header X-Encryption-Enabled: true to signal the backend that the body is encrypted.
X-Encryption-Enabled: Opt-In Encryption
Not every request needs encryption. GET requests, file uploads (multipart), and other scenarios are not suitable candidates. The X-Encryption-Enabled header provides an opt-in mechanism:
- The frontend adds
X-Encryption-Enabled: truewhen sending sensitive data - The backend filter chain only activates decryption when it detects this header
- Requests without the header pass through untouched
This design makes encryption an optional capability rather than a mandatory requirement. During development and debugging, you can skip encryption entirely. In production, sensitive endpoints enable it automatically.
The Backend Filter Chain: CachedBodyFilter + RSADecryptionFilter
Decryption on the backend is handled by two Servlet filters, registered with explicit ordering in FilterConfig:
Layer 1: CachedBodyFilter (Order 1)
Spring's HttpServletRequest.getInputStream() can only be read once. The decryption process needs to read the ciphertext first, then let the Controller read the decrypted plaintext. CachedBodyFilter solves this by caching POST, PUT, and PATCH request bodies into memory, wrapping them in a CachedBodyHttpServletRequest that supports repeated reads. Multipart file upload requests are automatically skipped.
Layer 2: RSADecryptionFilter (Order 2)
This filter checks the X-Encryption-Enabled header. If the value is true, it reads the encryption envelope from the cached body and performs decryption:
- Parse the JSON envelope to extract
ek(encrypted AES key),iv(initialization vector), andct(ciphertext) - Decrypt
ekusing the RSA private key to obtain the Base64-encoded AES key - Decrypt
ctusing the AES key and IV via AES-GCM mode to recover the original JSON - Create a new request wrapper with the decrypted data as the body and Content-Type set to
application/json;charset=UTF-8
String ek = root.get("ek").asText();
String keyBase64 = RSAUtil.decryptFromFrontend(ek);
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
byte[] ivBytes = Base64.getDecoder().decode(ivStr);
byte[] ctBytes = Base64.getDecoder().decode(ctStr);
decryptedData = AESUtil.decryptGCM(keyBytes, ivBytes, ctBytes);
After decryption, the Controller receives a standard JSON request body and does not need to be aware that encryption exists.
Response Signatures: X-Response-Signature
Encryption is one-directional (frontend to backend), but we added integrity verification for responses. The ResponseEncryptionAdvice uses the RSA private key to sign every response body with SHA256withRSA. The signature is returned via the X-Response-Signature header. The frontend's verifySignature function uses the RSA public key to verify the signature, ensuring the response has not been tampered with.
Transparent to Business Code
A key design goal of this encryption scheme is complete transparency to business logic. Controller-layer code requires zero modifications -- encryption and decryption happen entirely at the filter layer. Data entering and leaving Controllers is always standard JSON. When adding new API endpoints, encryption activates automatically as long as the frontend sets the correct header.
Summary
The hybrid encryption in Flash Warehouse WMS is a pragmatic engineering decision. Without JWT or session-based authentication, RSA key exchange combined with AES-GCM content encryption provides application-layer protection for sensitive warehouse data in transit. The X-Encryption-Enabled header enables opt-in activation, and the Spring Boot filter chain makes decryption completely transparent to business code. This design strikes a balance between security and developer convenience.