SpringBoot DB 암/복호화

Mo-Greene·2024년 4월 20일

개인정보와 같이 민감한 데이터는 DB에 저장 시 암/복호화 과정을 거쳐야 한다.

이번엔 간단하지만 DB 암/복호화에 대해서 어떻게 구현했는지 코드를 통해 알아보려 한다.


다른 회사에선 어떻게 구현하는지는 잘 모르고 직접 생각해보며 구현한 과정임을 밝힙니다.

@ColumnTransformer

참조 블로그

@Column(name="c3")
@ColumnTransformer(
		read = "convert_from(decrypt(decode(c3,'hex'),'ENC_KEY','aes'),'utf8')",
		write = "encode(encrypt(convert_to(?,'utf8'),'ENC_KEY','aes'),'hex')"
)
private String c3;

hibernate에서 지원해주는 @ColumnTransformer을 찾게 되었다.

위의 방법을 사용하게 되면 2가지의 문제점을 찾게 되었는데


1. key를 따로 관리할 수 없다.

어노테이션 내부의 ENC_KEY는 read와 write의 과정에서 나오는 key값이다.
보통 key값의 경우 보안적으로 따로 관리해야한다고 보는데(ex. application.yml)

해당 key는 하드코딩된 형태라 코드가 노출이 되었을 경우 보안에서 심각한 결함이 생기는 것이다.

관련 질문

참조 블로그와 답변에 나온것 처럼 DB function을 만들어 사용하게 된다면 나름대로 해결이 되겠지만
여기서 2번째 문제점이 생긴다.


2. 필드의 중복성
필드에 암/복호화를 진행해야할 필드가 늘어나면 늘어날 수록 하드코딩된 @ColumnTransformer를 적어주어야 한다.

만약 100개의 필드를 암/복호화 해야한다면?
key값이 변경이 된다면?
DB가 변경되어 Function 이름이 변경이 되었다면?

참많은 이유로 하드코딩된 @ColumnTransformer를 찾아 바꿔야 할 것이다.(어느 IDE에서는 한꺼번에 바꿀 수 있다는 건 답변이 되지 않는다.)

위의 사항들을 고려해보면서 생각한 과정은 바로..


@Converter

@Converter는 이미 여러 블로그에서 사용방법까지 설명하였으니 다루지 않도록 한다.

결국 Server에서 DB로 갈때 Converter를 거쳐 엔티티에 삽입되는 값을 encode, 또는 decode하면 될 것 같았다.

encode : Server(entity) --- @Converter ---> Database
decode : Database --- @Converter ---> Server

코드로 살펴보자


application.yml

aes:
  key: GymBaseSecretKey

관리할 key값을 설정하자. 주의할 점은 key의 길이인데 나의 경우 16바이트로 설정했다.
16, 24, 32 바이트 중에 정하도록 하자
(32바이트는 보안을 조금 더 강화하지만 암호를 해독하는 데 필요한 시간이 늘어난다.)


AESUtil

@Component
public class AESUtil {

	@Value("${aes.key}")
	private String keyValue;
	private static final String ALGORITHM = "AES";

	//encode
	public String encrypt(String valueToEnc) throws Exception {
		Key key = generateKey();
		Cipher c = Cipher.getInstance(ALGORITHM);
		c.init(Cipher.ENCRYPT_MODE, key);
		byte[] encValue = c.doFinal(valueToEnc.getBytes());
		
		return Base64.getEncoder().encodeToString(encValue);
	}
	
    //decode
	public String decrypt(String encryptedValue) throws Exception {
		Key key = generateKey();
		Cipher c = Cipher.getInstance(ALGORITHM);
		c.init(Cipher.DECRYPT_MODE, key);
		byte[] decodedValue = Base64.getDecoder().decode(encryptedValue);
		byte[] decValue = c.doFinal(decodedValue);
		
		return new String(decValue);
	}

	private Key generateKey() {
		return new SecretKeySpec(keyValue.getBytes(), ALGORITHM);
	}
}

AES의 바이트 설정을 application.yml에서 했기 때문에
generateKey() 메서드 내의 keySpec을 정하는 부분에서 저절로 16바이트 AES 암호화가 진행된다.


DatabaseConverter.java

@Converter
@RequiredArgsConstructor
public class DatabaseConverter implements AttributeConverter<String, String> {

	private final AESUtil aesUtil;

	//application --> database
	@Override
	public String convertToDatabaseColumn(String attribute) {
		try {
			return aesUtil.encrypt(attribute);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	//database --> application
	@Override
	public String convertToEntityAttribute(String dbData) {
		try {
			return aesUtil.decrypt(dbData);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
}

@Converter를 공부했다면 쉽게 구현이 가능하다.


Entity

public class Member extends BaseEntity {

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

	@Column(nullable = false, length = 30)
	@Convert(converter = DatabaseConverter.class)
	@Comment("회원 이름")
	private String username;

	@Column(nullable = false)
	@Convert(converter = DatabaseConverter.class)
	@Comment("주소")
	private String address;
}

이렇게 되면 하드코딩된 코드를 바꿀 필요없이 어노테이션만 뗐다 붙였다 하면서 관리할 수 있게 된다.


Test

	@Test
	void create_test() {
		// given
		Member member = Member.builder()
			.username("mo")
			.address("서울시 강남구")
			.build();

		// when
		Member savedMember = memberRepository.save(member);
		System.out.println("savedMember = " + savedMember.getUsername());
	}

결과

복호화의 경우 통과되었고 정상적으로 암호화 되었는지 DB를 보겠다.

@RestController
@RequestMapping("/api/hello")
@RequiredArgsConstructor
public class HelloController {

	private final MemberRepository memberRepository;

	@GetMapping
	public ResponseEntity<String> getHello() {
		Member member = Member.builder()
			.username("Mo-Greene")
			.address("Seoul")
			.build();
		Member result = memberRepository.save(member);
		String message = result.getUsername() + " 님 확인, " + result.getCreatedAt() + "에 생성되었습니다.";
		return new ResponseEntity<>(message, HttpStatus.OK);
	}
}

간단하게 api 테스트 Controller를 만들었고 테스트해봤다.

정상적으로 암/복호화 테스트를 완료하였다.

profile
아둥바둥 버텨라

2개의 댓글

comment-user-thumbnail
2024년 4월 21일

안녕하세요. 글 잘 보았습니다. :) 제 생각에는 BCrypt로 해시 처리를 하는 것 정도로 고려를 할 것 같은데, 이렇게 구현을 시도하신 이유가 있으실까요 ? (궁금함에 여쭈어봅니다. 좋은하루되세요!)

1개의 답글