- Project Structure
- Prerequisites
- How to Start the Services
- Encryption Concepts Explained
- How Obfuscation Works
- Endpoints and Manual Curls
- Running the Full Test Script
- Applied Best Practices
- Data Flow Diagram
dac-crypto-demo/
βββ pom.xml β Root POM (multi-module)
βββ test-api.sh β Full test script
β
βββ crypto-service/ β Port 8081
β βββ pom.xml
β βββ src/main/java/com/bcp/dac/crypto/
β βββ model/CryptoModels.java β Request/response records
β βββ service/
β β βββ KeyStoreService.java β RSA key management
β β βββ CryptoService.java β AES-GCM + RSA-OAEP
β β βββ ObfuscationService.java β Masking
β βββ resource/CryptoResource.java β REST endpoints
β
βββ account-service/ β Port 8082
β βββ pom.xml
β βββ src/main/java/com/bcp/dac/account/
β βββ client/CryptoClient.java β REST Client β crypto-service
β βββ model/AccountModels.java
β βββ service/AccountService.java
β βββ resource/AccountResource.java
β
βββ identity-service/ β Port 8083
βββ pom.xml
βββ src/main/java/com/bcp/dac/identity/
βββ resource/IdentityResource.java
| Tool | Minimum Version | Check |
|---|---|---|
| Java JDK | 21 | java --version |
| Maven | 3.9.x | mvn --version |
| curl | any | curl --version |
| jq | any | jq --version (optional, for JSON format) |
Open 3 separate terminals.
cd dac-crypto-demo/crypto-service
mvn quarkus:devQuarkus Dev Mode includes:
- Automatic hot reload (code changes apply without restarting)
- Swagger UI at http://localhost:8081/swagger-ui
- Dev UI at http://localhost:8081/q/dev
cd dac-crypto-demo/account-service
mvn quarkus:devcd dac-crypto-demo/identity-service
mvn quarkus:devcurl http://localhost:8081/health/live
curl http://localhost:8082/health/live
curl http://localhost:8083/health/liveAES = Advanced Encryption Standard
256 = key size in bits
GCM = Galois/Counter Mode (operation mode)
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AES-256-GCM = Encryption + Authentication in one step β
β β
β Inputs: β
β plaintext = "4111111111111234" β
β CEK = 32 random bytes (secret key) β
β IV = 12 random bytes (unique per operation) β
β β
β Outputs: β
β ciphertext = encrypted bytes β
β authTag = 16-byte integrity signature β
β β
β If someone modifies the ciphertext β invalid authTag β
β β decryption fails β tampering is detected β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why GCM and not CBC?
- CBC only encrypts. If someone modifies the ciphertext, you won't know.
- GCM encrypts AND authenticates (AEAD = Authenticated Encryption with Associated Data).
RSA = RivestβShamirβAdleman
2048 = key size in bits
OAEP = Optimal Asymmetric Encryption Padding
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β HOW THE KEY PAIR WORKS β
β β
β K-pub (public): ANYONE can use it to encrypt β
β K-priv (private): ONLY the owner can decrypt β
β β
β Encrypt with K-pub: β
β RSA_OAEP(CEK, K-pub) β encryptedCek β
β β
β Decrypt with K-priv: β
β RSA_OAEP(encryptedCek, K-priv) β CEK β
β β
β If someone intercepts encryptedCek: β
β Without K-priv β cannot obtain the CEK β
β Without the CEK β cannot decrypt the data β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Why not use RSA to encrypt the data directly?
- RSA-2048 can only encrypt ~190 bytes. A JSON payload may be larger.
- RSA is ~1000x slower than AES.
- Solution: AES encrypts the data (fast), RSA encrypts only the AES key (small).
ENCRYPTION:
plaintext = "4111111111111234"
β
βββΊ [1] Generate random CEK (AES-256, 32 bytes, ephemeral)
β CEK = a1b2c3d4e5f6... (never reused)
β
βββΊ [2] AES_GCM(plaintext, CEK, IV) β ciphertext + authTag
β
βββΊ [3] RSA_OAEP(CEK, K-pub) β encryptedCek
β
βββΊ [4] Envelope = {
encryptedCek, β RSA(CEK)
ciphertext, β AES(data)
iv, β initialization vector
authTag, β integrity seal
keyId β "rsa-key-v1"
}
DECRYPTION (only with K-priv):
Envelope β RSA_OAEP(encryptedCek, K-priv) β CEK
β AES_GCM(ciphertext, CEK, iv, authTag) β plaintext
Why an ephemeral CEK?
If a CEK is compromised, only ONE piece of data is exposed.
With a single key for everything, one compromise exposes EVERYTHING.
Obfuscation is visual masking, NOT encryption.
| Type | Input | Output | Technique |
|---|---|---|---|
| CARD_NUMBER | 4111111111111234 | 411111******1234 | PCI-DSS: first 6 + last 4 digits |
| ACCOUNT_NUMBER | 19302938471923 | **********1923 | Last 4 digits visible |
| NATIONAL_ID | 12345678 | ****5678 | Last 4 digits visible |
| juan@gmail.com | j***n@gmail.com | First + last char of the local part |
When to use each:
| Tool | When to use |
|---|---|
| Encryption | When you need to recover the original data |
| Obfuscation | For logs, traces, UI (no recovery needed) |
| SHA-256 Hash | For searches without exposing the data (email lookup) |
curl -X POST http://localhost:8081/api/v1/crypto/encrypt \
-H "Content-Type: application/json" \
-d '{
"plaintext": "4111111111111234",
"dataType": "CARD_NUMBER",
"contextInfo": "purchase-payment-001"
}'Response:
{
"encryptedCek": "TW96aSBGaXJlZm94...(Base64, ~344 chars)",
"ciphertext": "xK9mP2...(Base64)",
"iv": "abc123...(Base64, 16 chars)",
"authTag": "def456...(Base64, 24 chars)",
"keyId": "rsa-key-v1",
"dataType": "CARD_NUMBER",
"maskedPreview":"411111******1234"
}# Use the values from the previous step
curl -X POST http://localhost:8081/api/v1/crypto/decrypt \
-H "Content-Type: application/json" \
-d '{
"encryptedCek": "<encryptedCek from previous step>",
"ciphertext": "<ciphertext from previous step>",
"iv": "<iv from previous step>",
"authTag": "<authTag from previous step>",
"keyId": "rsa-key-v1",
"dataType": "CARD_NUMBER"
}'Response:
{
"plaintext": "4111111111111234",
"maskedPreview": "411111******1234",
"dataType": "CARD_NUMBER",
"integrityVerified": true
}curl -X POST http://localhost:8081/api/v1/crypto/obfuscate \
-H "Content-Type: application/json" \
-d '{
"value": "juan.perez@gmail.com",
"dataType": "EMAIL"
}'curl -X POST http://localhost:8082/api/v1/accounts \
-H "Content-Type: application/json" \
-d '{
"holderName": "Juan Pablo PΓ©rez",
"accountNumber": "19302938471923",
"cardNumber": "4111111111111234",
"accountType": "SAVINGS"
}'curl http://localhost:8082/api/v1/accounts# Replace {accountId} with the ID obtained when creating
curl http://localhost:8082/api/v1/accounts/{accountId}curl -X POST http://localhost:8082/api/v1/accounts/{accountId}/reveal \
-H "Content-Type: application/json" \
-d '{"field": "accountNumber"}'curl -X POST http://localhost:8082/api/v1/accounts/{accountId}/reveal \
-H "Content-Type: application/json" \
-d '{"field": "cardNumber"}'curl -X POST http://localhost:8083/api/v1/identities \
-H "Content-Type: application/json" \
-d '{
"fullName": "Juan Pablo PΓ©rez Camacho",
"nationalId": "12345678",
"email": "jperez@bcp.com.pe"
}'curl http://localhost:8083/api/v1/identitiescurl -X POST http://localhost:8083/api/v1/identities/{identityId}/reveal \
-H "Content-Type: application/json" \
-d '{"field": "nationalId"}'curl -X POST http://localhost:8083/api/v1/identities/{identityId}/reveal \
-H "Content-Type: application/json" \
-d '{"field": "email"}'# Grant execution permissions
chmod +x test-api.sh
# Run (requires all 3 services UP)
./test-api.shThe script runs in order:
- Health checks for all 3 services
- Obfuscation tests (4 data types)
- Direct encryption and decryption + tampering test
- Full Account Service flow
- Full Identity Service flow
- Displays Swagger UI URLs
- Never log sensitive data in plaintext: all logs use
maskedPreview - Ephemeral CEK per operation: breach impact is localized
- Unique random IV: never reuse an IV with the same key
- AES-GCM over AES-CBC: built-in authentication
- RSA-OAEP over RSA-PKCS1v1.5: resistant to Bleichenbacher attacks
- SecureRandom: never use
Math.random()ornew Random()for cryptography - char[] over String: decrypted data in memory should be zero-outable (String is immutable in Java)
- Java 21 Records: immutable models by design
- @ApplicationScoped: singletons for stateful services (KeyStore)
- Domain exceptions:
CryptoException,AccountNotFoundExceptioninstead of exposing JCA exceptions - Validation with @Valid + @NotBlank: never trust client input
- Version in URL (
/api/v1/): enables evolution without breaking clients - Layer separation: Resource β Service β Client (simplified hexagonal)
- Structured logs with level and class
- maskedPreview in all audit operations
- Swagger UI enabled for development (
quarkus.swagger-ui.always-include=true) - Health checks liveness + readiness with SmallRye Health
- JWT authentication on all endpoints
- Real HashiCorp Vault / Azure Key Vault (simulated in-memory here)
- Real persistence with JPA/Panache + database
- mTLS between microservices
- Rate limiting on
/revealendpoints - OpenTelemetry for distributed tracing
- Automatic key rotation
Client (Browser/App)
β HTTPS/TLS 1.3
βΌ
Account Service (8082)
β
βββΊ 1. Receives { accountNumber: "193029...", cardNumber: "4111..." }
β
βββΊ 2. Calls Crypto Service: POST /encrypt { plaintext: "193029..." }
β Crypto Service returns: { encryptedCek, ciphertext, iv, authTag, keyId }
β
βββΊ 3. Calls Crypto Service: POST /encrypt { plaintext: "4111..." }
β Crypto Service returns: { encryptedCek, ciphertext, iv, authTag, keyId }
β
βββΊ 4. Persists ONLY the encrypted envelopes (never plaintext data)
β
βββΊ 5. Returns to client: { maskedAccountNumber: "**1923", maskedCardNumber: "411111**1234" }
Subsequent query:
GET /accounts/{id} β returns masked data + encrypted envelopes
POST /accounts/{id}/reveal β calls Crypto Service to decrypt