feat(security): add support for message encryption with a key

This commit is contained in:
Pablo de la Torre Jamardo 2025-09-15 08:25:06 +02:00
parent c541119cf0
commit d417a46a06
5 changed files with 102 additions and 0 deletions

View File

@ -1,5 +1,7 @@
SPRING_PROFILES_ACTIVE=dev
APP_ENCRYPTION_SECRET=123456789
DB_NAME=EXAMPLE_DB
DB_USER=EXAMPLE
DB_PASSWORD=SECRET

View File

@ -1,6 +1,9 @@
info:
app:
version: @project.version@
app:
encryption:
secret: ${APP_ENCRYPTION_SECRET}
spring:
application:
name: restemailbridge
@ -26,6 +29,7 @@ springdoc:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui
show-actuator: true
server:
port: 8080

View File

@ -13,6 +13,7 @@ services:
DB_PORT: ${DB_PORT}
DB_USER: ${DB_USER}
DB_PASSWORD: ${DB_PASSWORD}
APP_ENCRYPTION_SECRET: ${APP_ENCRYPTION_SECRET}
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
networks:

View File

@ -0,0 +1,23 @@
package com.pablotj.restemailbridge.infrastructure.persistence;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import org.springframework.beans.factory.annotation.Value;
@Converter
public class EncryptionConverter implements AttributeConverter<String, String> {
@Value("${app.encryption.secret}")
private String secret;
@Override
public String convertToDatabaseColumn(String attribute) {
return attribute == null ? null : new EncryptionUtils(secret).encrypt(attribute);
}
@Override
public String convertToEntityAttribute(String dbData) {
return dbData == null ? null : new EncryptionUtils(secret).decrypt(dbData);
}
}

View File

@ -0,0 +1,72 @@
package com.pablotj.restemailbridge.infrastructure.persistence;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
public class EncryptionUtils {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
private static final int TAG_LENGTH_BIT = 128;
private static final int IV_LENGTH_BYTE = 12;
private static final SecureRandom secureRandom = new SecureRandom();
private final SecretKey secretKey;
public EncryptionUtils(String secret) {
if (secret == null || secret.getBytes(StandardCharsets.UTF_8).length != 32) {
throw new IllegalArgumentException("Secret key must be 32 bytes for AES-256");
}
this.secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), ALGORITHM);
}
public String encrypt(String plainText) {
try {
byte[] iv = new byte[IV_LENGTH_BYTE];
secureRandom.nextBytes(iv);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, spec);
byte[] cipherText = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
// Guardamos IV + ciphertext juntos
ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + cipherText.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
return Base64.getEncoder().encodeToString(byteBuffer.array());
} catch (Exception e) {
throw new RuntimeException("Failed to encrypt text", e);
}
}
public String decrypt(String base64CipherText) {
try {
byte[] cipherMessage = Base64.getDecoder().decode(base64CipherText);
ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
byte[] iv = new byte[IV_LENGTH_BYTE];
byteBuffer.get(iv);
byte[] cipherText = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherText);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec);
byte[] plainText = cipher.doFinal(cipherText);
return new String(plainText, StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to decrypt text", e);
}
}
}