diff --git a/.env.example b/.env.example index 1b34b9e..e781ca2 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ SPRING_PROFILES_ACTIVE=dev +APP_ENCRYPTION_SECRET=123456789 + DB_NAME=EXAMPLE_DB DB_USER=EXAMPLE DB_PASSWORD=SECRET diff --git a/bootstrap/src/main/resources/application.yml b/bootstrap/src/main/resources/application.yml index 6903273..8b56cac 100644 --- a/bootstrap/src/main/resources/application.yml +++ b/bootstrap/src/main/resources/application.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index 84933eb..0d8a202 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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: diff --git a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionConverter.java b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionConverter.java new file mode 100644 index 0000000..5897474 --- /dev/null +++ b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionConverter.java @@ -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 { + + + @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); + } +} diff --git a/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionUtils.java b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionUtils.java new file mode 100644 index 0000000..eeec59c --- /dev/null +++ b/infrastructure/src/main/java/com/pablotj/restemailbridge/infrastructure/persistence/EncryptionUtils.java @@ -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); + } + } +}