이끌든지 따르든지 비키든지

Framework/JPA

[JPA] @Converter 암호화를 사용해 데이터베이스 보안 강화하기

SeongHo5 2024. 8. 24. 17:55

데이터베이스에 중요한 정보를 보관할 때, 보안은 필수적인 요소입니다.

 

 특히, 개인정보와 같은 민감한 데이터를 저장할 때는 암호화를 통해 보안을 강화하는 것이 중요합니다. 

 

JPA를 사용하여 데이터베이스에 저장되는 데이터를 안전하게 암호화하고 복호화하는 방법에 대해 알아보겠습니다.

 

 

@ColumnTransformer


 

첫 번째 방법은, @ColumnTransformer 어노테이션을 사용하는 것입니다.

 

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "email")
    @ColumnTransformer(
        read = "AES_DECRYPT(UNHEX(email), 'key-here')",
        write = "HEX(AES_ENCRYPT(?, 'key-here'))"
    )
    private String email;

    // 기타 생략
}

 

 

데이터베이스가 제공하는 함수를 활용해 DB 레벨에서 데이터를 암호화하거나 복호화하는 등의 변환을 적용할 수 있습니다.

 

어노테이션 기반이기 때문에, 선언만으로 암호화, 복호화를 자동으로 처리할 수 있어 편리함이 있습니다.

 

다만, SQL을 기반으로 동작하기에 특정 데이터베이스에 종속되고, 단위 테스트 구성이 어렵다는 단점이 있습니다.

 

 

AttributeConverter


 

빠른 구현이 필요하다면 @ColumnTransformer를 활용할 수 있지만, 이러한 단점을 보완하고, 애플리케이션 레벨에서의 세밀한 제어를 원한다면,  AttributeConverter를 사용할 수 있습니다.

 

 

구현 방법

 

1. Converter 구현

 

먼저, 암호화(애플리케이션 → DB) / 복호화(DB → 애플리케이션)를 처리할 Converter를 구현해야 합니다.

 

@Converter
public class StringEncryptionConverter implements AttributeConverter<String, String> {

  private static final String ENCRYPTION_TRANSFORM = "AES/GCM/NoPadding";

  private static final int AES_KEY_LENGTH = 16;

  private static final int INITIAL_VECTOR_LENGTH = 12;

  private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;

  private Cipher cipher;

  private SecretKeySpec secretKeySpec;

  @Value("${enncryption.key}")
  private String encryptionKey;

  @PostConstruct
  public void init() {
    try {
      this.cipher = Cipher.getInstance(ENCRYPTION_TRANSFORM);
      this.secretKeySpec = generateSecretKey(this.encryptionKey);
    } catch (GeneralSecurityException e) {
      // 예외 처리
    }
    
  private static SecretKeySpec generateSecretKey(final String key) {
    byte[] keyBytes = key.getBytes(DEFAULT_ENCODING);
    final byte[] finalKey = new byte[AES_KEY_LENGTH];

    for (int i = 0; i < finalKey.length; i++) {
      finalKey[i] = keyBytes[i % keyBytes.length];
    }

    return new SecretKeySpec(finalKey, "AES");
  }
  
}

 

 

AttributeConverter 제네릭은 각각 [엔티티에 정의된 타입] 과 [DB 컬럼의 타입]입니다.

 

저는 문자열 암·복호화를 위한 Converter를 구현하려는 목적이기 때문에 <String, String>으로 선언했지만, 암·복호화 외에도 다양한 변환 작업을 수행할 수 있습니다.

 

상수 필드 설명

  • ENCRYPTION_TRANSFORM: 암호화 시, 어떤 알고리즘과 모드 등을 사용할지 정의하는 문자열입니다.
  • AES_KEY_LENGTH: AES 알고리즘의 키 길이, AES128을 사용했기 때문에 16으로 정의했습니다.
  • INITIAL_VECTOR_LENGTH: 초기화 벡터*의 길이
  • DEFAULT_ENCODING: 인코딩 방식을 정의하는 문자열입니다.

 

💡초기화 벡터(IV, Initial Vector)란?
AES와 같은 대칭 키 암호화 방식에서는 동일한 키로 같은 평문을 암호화하면 똑같은 암호문이 생성되는데, 이는 보안 취약점이 될 수 있습니다. 이 문제를 해결하기 위해 초기화 벡터를 사용합니다.
초기화 벡터는 암호화할 때마다 변경되며, 같은 평문이라도 다른 IV를 사용하면 서로 다른 암호문이 생성됩니다. (GCM 모드에서는 일반적으로 12 bytes)
일반적으로, IV 값은 암호화할 때마다 매번 새로 생성하거나, 변경되어야 높은 보안을 유지할 수 있습니다.

 

 

암·복호화에 필요한 상수를 위와 같이 정의해 주고, secretKeySpec과 cipher를 초기화합니다.

 

 

암호화(애플리케이션 → DB) 메서드 구현

@Override
  public String convertToDatabaseColumn(String attribute) {
    if (attribute == null) {
      return null;
    }
    try {
      final byte[] initialVector = RandomGenerator.generateRandomBytes(INITIAL_VECTOR_LENGTH);
      final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initialVector);
      this.cipher.init(Cipher.ENCRYPT_MODE, this.secretKeySpec, gcmParameterSpec);

      byte[] encryptedData = this.cipher.doFinal(attribute.getBytes(DEFAULT_ENCODING));
      byte[] encryptedWithIv =
          ByteBuffer.allocate(initialVector.length + encryptedData.length)
              .put(initialVector)
              .put(encryptedData)
              .array();

      return Base64.toBase64String(encryptedWithIv);
    } catch (GeneralSecurityException e) {
      // 예외 처리
    }
  }

 

  1. 입력 값이 null인 경우, DB에 그대로 null로 저장될 수 있도록 바로 null을 반환합니다.
  2. 초기화 벡터 및 GCMParameterSpec 생성
    1. 일정한 길이(`INITIAL_VECTOR_LENGTH`)의 초기화 벡터를 생성합니다. 저는 기존에 있던 유틸리티 클래스의 SecureRandom.nextBytes() 메서드를 활용해 무작위 바이트 배열을 생성했습니다.
    2. 생성한 초기화 벡터로 GCMParameterSpec 생성
  3. Cipher 초기화 및 데이터 암호화
    1. 데이터 암호화를 위해 secretKeySpec과 gcmParameterSpec을 사용해 Cipher를 암호화 모드 (`ENCRYPT_MODE`)로 초기화합니다.
    2. 입력 값(`attribute`)을 바이트 배열로 변환한 후 암호화를 진행합니다.
  4. 초기화 벡터와 암호화된 데이터 결합
    1. 복호화를 위해 초기화 벡터와 암호화된 데이터를 순서대로 넣고, 최종적으로 결합된 배열을 생성합니다.
  5. DB 저장을 위해 Base64 인코딩 처리 후 문자열 반환합니다.


 

 

복호화(DB → 애플리케이션)

  @Override
  public String convertToEntityAttribute(String dbData) {
    if (dbData == null) {
      return null;
    }
    try {
      byte[] decodedData = Base64.decode(dbData);
      ByteBuffer byteBuffer = ByteBuffer.wrap(decodedData);

      byte[] initialVector = new byte[INITIAL_VECTOR_LENGTH];
      byteBuffer.get(initialVector);

      byte[] encryptedData = new byte[byteBuffer.remaining()];
      byteBuffer.get(encryptedData);

      final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(128, initialVector);
      this.cipher.init(Cipher.DECRYPT_MODE, this.secretKeySpec, gcmParameterSpec);

      byte[] decryptedData = this.cipher.doFinal(encryptedData);
      return new String(decryptedData, DEFAULT_ENCODING);
    } catch (GeneralSecurityException e) {
      // 예외 처리
    }
  }

 

 

복호화는 위 암호화 과정을 반대로 수행해 다시 평문으로 되돌리는 과정으로 진행됩니다.

 

  1. Base64로 인코딩 된 문자열을 decode 해 바이트 배열로 변환합니다.
  2. 초기화 벡터와, 암호화된 실제 데이터를 분리합니다.
    1. 암호화 시, 데이터 결합 과정에서 초기화 벡터를 앞에 위치하도록 결합했으므로, 초기화 벡터 길이( `INITIAL_VECTOR_LENGTH`)를 기준으로 분리해 각각 값을 분리할 수 있습니다.
  3. Cipher 초기화 및 데이터 복호화
    1. 데이터를 평문으로 되돌려야 하므로, 이번에는 cipher 객체를 복호화 모드(`DECRYPT_MODE`)로 초기화합니다.
    2. 암호화된 바이트 배열을 평문 바이트 배열로 복호화합니다.
  4. 복호화된 바이트 배열을 인코딩 방식에 따라 문자열로 복원합니다.

 

 

2. Converter 적용

 

Converter 구현이 완료됐다면, 암호화를 적용할 Entity 컬럼에 @Convert 어노테이션을 선언하는 것으로 간단하게 Converter를 적용할 수 있습니다.

 

@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "email")
    @Convert(StringEncryptionConverter.class)
    private String email;

    // 기타 생략
}

 

 

또는, @Converter 어노테이션에 `autoApply = true` 옵션으로, 모든 엔티티의 `String` 타입 필드에 Converter를 적용할 수 도 있습니다.

 

@Converter(autoApply = true)
public class StringEncryptionConverter implements AttributeConverter<String, String> {
	// 생략
  
}

 

'Framework > JPA' 카테고리의 다른 글

[JPA] 패러다임 불일치  (0) 2024.01.04
[JPA] JPA(Java Persistence API)란?  (0) 2024.01.02