feat(security): add support for message encryption with a key
This commit is contained in:
parent
c541119cf0
commit
d417a46a06
@ -1,5 +1,7 @@
|
|||||||
SPRING_PROFILES_ACTIVE=dev
|
SPRING_PROFILES_ACTIVE=dev
|
||||||
|
|
||||||
|
APP_ENCRYPTION_SECRET=123456789
|
||||||
|
|
||||||
DB_NAME=EXAMPLE_DB
|
DB_NAME=EXAMPLE_DB
|
||||||
DB_USER=EXAMPLE
|
DB_USER=EXAMPLE
|
||||||
DB_PASSWORD=SECRET
|
DB_PASSWORD=SECRET
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
info:
|
info:
|
||||||
app:
|
app:
|
||||||
version: @project.version@
|
version: @project.version@
|
||||||
|
app:
|
||||||
|
encryption:
|
||||||
|
secret: ${APP_ENCRYPTION_SECRET}
|
||||||
spring:
|
spring:
|
||||||
application:
|
application:
|
||||||
name: restemailbridge
|
name: restemailbridge
|
||||||
@ -26,6 +29,7 @@ springdoc:
|
|||||||
path: /v3/api-docs
|
path: /v3/api-docs
|
||||||
swagger-ui:
|
swagger-ui:
|
||||||
path: /swagger-ui
|
path: /swagger-ui
|
||||||
|
show-actuator: true
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8080
|
||||||
|
@ -13,6 +13,7 @@ services:
|
|||||||
DB_PORT: ${DB_PORT}
|
DB_PORT: ${DB_PORT}
|
||||||
DB_USER: ${DB_USER}
|
DB_USER: ${DB_USER}
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
DB_PASSWORD: ${DB_PASSWORD}
|
||||||
|
APP_ENCRYPTION_SECRET: ${APP_ENCRYPTION_SECRET}
|
||||||
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
|
GMAIL_OAUTH_CLIENT_ID: ${GMAIL_OAUTH_CLIENT_ID}
|
||||||
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
|
GMAIL_OAUTH_CLIENT_SECRET: ${GMAIL_OAUTH_CLIENT_SECRET}
|
||||||
networks:
|
networks:
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user