DevYGwan
DB에 저장하는 데이터는 개발자도 모르게!! 본문
데이터를 암호화하는 것은 필수적입니다. 특히 요즘 같이 데이터가 곧 힘인 시대에서는 더욱 더 암호화를 신경써서 해야됩니다. 개인정보 보호 뿐만아니라 내가 사용자로써 앱을 사용할때 해당 앱이 데이터가 암호화되어있지 않다면 사용하기 꺼려질 것 같습니다. 만약 악의적인 공격자가 저희 서버의 디비를 탈취하여 디비를 조회했을때 사용자 데이터가 바로 노출되면 큰 문제가 될 것이기 때문입니다. 따라서 이번에 프로젝트를 진행하면서 유저들의 데이터를 제대로 암호화하기 위해 노력했습니다.
처음에는 모든 데이터들을 암호화 처리하였습니다. 그러다보니 모니터링 할때 어떤 유저의 데이터인지 확인하기 어려워 불편했습니다. 따라서 모든 데이터를 암호화하는 것이 아닌 기준을 가지고 암호화를 처리했습니다.
데이터 암호화를 처리한 기준은 다음과 같습니다.
1. 사용자 개인 데이터인가?
2. 만약 데이터가 유출됐을때 민감한 데이터인가?
3. 앱 내에서 사용자를 인증할때 쓰는 데이터인가?
이런 기준으로 데이터 암호화 여부를 결정하였습니다. 아래는 저희 프로젝트의 멤버 관련 디비에 저장되는 컬럼입니다. 이 중 저는 birth, kakao_id, major, password, salt, profile_photo, university, university_email 이렇게 8개의 데이터를 암호화 처리 했습니다. 그밖의 데이터는 사용자 개인 데이터가 아니고 앱 내에서만 의미 있는 데이터이기 때문에 암호화처리를 하지 않았습니다. 또한 개발자가 디비 모니터링을 할때 유저 정보를 식별하기 위해 username하고 gender 정보를 남겨 식별할 수 있도록 암호화를 따로 진행하지 않았습니다.
암호화를 진행할때 중점적으로 둔 사항은 다음과 같습니다.
1. 복호화가 필요한 데이터인지 확인하기
2. 서비스 로직과 암호화를 분리해 서비스 로직에서는 암호화를 생각하지 않게 하기
3. 어떤 암호화 알고리즘을 사용하고 어떤 방식으로 사용해야 더 보안적으로 유리한지 확인하기
1. 복호화가 필요한 데이터인지 확인하기
복호화가 필요한 데이터인지 생각한 이유는 알고리즘의 선택 때문입니다. 복호화가 필요하지 않은 데이터는 굳이 복호화를 처리할 필요가 없기 때문에 단방향 알고리즘을 사용하는 것이 더 보안적으로 유리하기 때문입니다. 또한 복호화를 하지 않는다면 서버 개발자들도 유저의 실제 데이터를 확인하기 어려울 것이기 때문에 그것도 의미가 있다고 생각했습니다.
패스워드 같은 경우에는 굳이 원본 데이터를 확인할 필요가 없습니다. 정확히 말하면 원본 데이터를 확인할 수 있어서는 안됩니다. 패스워드를 인증할 때는 그 암호화된 패스워드를 가지고 인증 처리를 하면 되기 때문입니다. 이럴 경우에는 단방향 암호화 기법을 사용했습니다. 이렇게되면 서버 개발자들조차 사용자의 패스워드를 확인하기가 어려워집니다. 복호화를 하지 않기 때문입니다. 따라서 이것 또한 의미가 있다고 생각했습니다.
반대로, 다른 유저 데이터들은 실제 조회하는 경우가 있습니다. 유저 페이지에서 대학교나 생일, 전공 등의 데이터를 조회할 수 있도록 로직을 구현했기 때문입니다. 이때 만약 복호화를 할 수 없는 단방향 알고리즘을 사용한다면, 유저 페이지에서 조회할때 암호화된 데이터로 보여질 것이고 그건 문제가 되기 때문입니다. 따라서 알고리즘 선택을 위해 복호화 필요 여부를 확인했습니다.
만약 단방향 알고리즘과 양방향 알고리즘에 개념에 대해서 모르실 수도 있을 것이라고 판단해 간단히 정리하자면,
단방향 알고리즘 :
- 데이터를 암호화할 때 일방향으로만 변환되어, 원래의 데이터를 복원할 수 없는 방식
- 주로 복호화 할 필요가 없는 비밀번호 저장 등에 사용됩니다.
양방향 알고리즘 :
- 양방향 암호화 기법은 데이터를 암호화할 때 변환된 데이터를 다시 원래의 데이터로 복원할 수 있는 방식
- 주로 데이터의 기밀성을 보호하거나 안전한 통신을 위해 사용됩니다.
2. 서비스 로직과 암호화를 분리해 서비스 로직에서는 암호화를 생각하지 않게 하기
서비스 로직에서 암호화 처리를 신경쓰게 된다면 서비스 로직에서 처리해야할 비즈니스 로직에 집중할 수 없을 것이라고 생각해 이를 분리하기 위해 노력했습니다. 그렇게되면 암호화 로직을 담당할때는 암호화 로직만 담당할 수 있고 비즈니스 로직을 담당할때는 비즈니스 로직만 담당할 수 있기 때문입니다.
encryptService를 생성하고 암호화나 복호화 관련된 처리를 진행했습니다. 이 클래스 내에서 암호화 복호화 처리를 진행합니다. 패스워드 같은 경우에는 로그인 & 회원가입 & 패스워드 변경시에 해당 클래스의 encryptedPassword() 메서드를 사용해서 처리합니다.
유저 데이터의 경우 DB를 기준으로 DB에 저장될때는 암호화를, DB에서 나올때는 복호화를 처리해 조회하는 동일한 매커니즘으로 처리됩니다. 따라서 Spring에서 제공하는 Converter를 사용해서 이를 구혀했습니다. 이렇게 해서 저는 서비스로직과 암호화 로직을 분리해 처리했습니다.
3. 어떤 암호화 알고리즘을 사용하고 어떤 방식으로 사용해야 더 보안적으로 유리한지 확인하기
password의 경우 단방향 알고리즘 중 SHA-256을 사용했고 나머지 유저 데이터의 경우 양방향 알고리즘인 AES를 사용했습니다.
SHA-256을 사용한 이유는 안전한 해시 함수이고 계산이 비교적 빠르게 이루어지는 암호화 알고리즘이기 때문입니다. 하지만 SHA-256은 레인보우 테이블 문제가 있습니다. 따라서 추가적으로 salt를 사용해 보안을 더 강화했습니다. AES를 사용한 이유는 가장 안전하게 알려진 대칭키 암호화 알고리즘 중 하나이고 미국 NIST에 의해 표준으로 지정된 알고리즘이기 때문에 안전하다고 생각했습니다. 또한 처리 속도도 빨라 암호화 / 복호화 작업에 유리하기 때문입니다.
최종 결과입니다. 보시는 바와 같이 다 암호화 처리가 되어 있어 데이터가 유실되더라도 직접적인 유저 데이터를 알 수 없습니다. 하지만 username, 성별 등의 데이터는 따로 암호화 처리를 하지 않아 이렇게 DB를 모니터링 하더라도 해당 row의 데이터가 어떤 유저의 데이터인지 확인할 수 있습니다.
그렇다면 지금부터 password 암호화 알고리즘 & 유저 데이터 암호화 알고리즘을 어떤식으로 구현했는지 설명하도록 하겠습니다.
※ Password 암호화 알고리즘
패스워드 암호화 알고리즘은 위에서 말한대로 SHA-256 알고리즘을 사용했습니다. SHA-256 알고리즘은 java.security.MessageDigest 클래스를 사용하여 구현할 수 있습니다. 하지만 이 해시 알고리즘은 문제가 있습니다. 해시 함수의 경우 동일한 입력에 대해 항상 동일한 해시값이 생성됩니다. 이는 결국 레인보우 테이블 공격을 통해 hash 값으로 통해 평문의 패스워드를 획득할 수 있다는 문제가 발생합니다. 따라서 이러한 문제를 해결하기 위해 salt, pepper라는 개념이 나오게 되었습니다. 저는 salt를 사용해서 패스워드를 저장할때 임의의 난수를 생성하고 그 값과 유저가 입력한 평문 패스워드를 합쳐 이를 암호화해 저장했습니다. 저장할때는 암호화된 패스워드와 생성한 salt값을 같이 저장했습니다. 이후에 로그인 시에는 입력된 패스워드 값과 유저의 salt 값을 조회한 후 이를 합쳐 암호화하고 저장된 암호화된 패스워드 값과 비교하여 인증 처리를 진행했습니다. 이렇게 함으로써, 유저마다 각각 다른 임의의 난수값이 생성되고 이 값과 원본 패스워드 평문값을 합쳐 암호화하기 때문에 레인보우 테이블 공격을 막을 수 있습니다.
다음은 간단하게 사용한 용어들을 정리해도록 하겠습니다.
용어 | 설명 |
레인보우 테이블 | 레인보우 테이블은 해시 함수(MD5, SHA-1, SHA-2 등)을 사용하여 만들어낼 수 있는 값들을 저장한 표. |
Salt | 각각의 입력 값에 대응하는 임의로 생성된 데이터 (랜덤 값) |
Pepper | 모든 입력 값에 공통으로 사용되는 데이터 (고정된 값) |
그렇게 해서 password 암호화 로직을 구현했습니다. 아래는 member DB에서 password, salt에 관한 데이터 예시입니다. 이런식으로 패스워드 관련 데이터가 저장됩니다. 여기서 더 보안적으로 강화하려면 salt 정보를 따로 저장하는 것도 하나의 좋은 방법이라고 생각합니다.
※ 유저 데이터 암호화 알고리즘
유저 데이터 암호화 알고리즘은 양방향 암호화 방식인 AES를 사용했습니다. 암호화하고 복호화를 하기 위해 key값이 필요하고 key값을 노출해서는 안됩니다. 따라서 key값은 yaml파일에서 환경변수로 관리했습니다. 또한 이 key에 대한 값도 암호화 처리를 하기 때문에 이 key또한 AES로 암호화를 진행했습니다. AES의 경우 초기화 벡터인 IV(Initialization Vector)가 필요합니다. IV는 암호화 알고리즘의 안전성과 보안성을 향상시키기 위해 사용되기 때문입니다. IV의 경우 AES의 키 값에서 16바이트 만큼을 떼서 사용했습니다.
유저 데이터 암호화 알고리즘을 좀 더 자세히 설명하자면 'AES/CBC/PKCS5Padding' 이 방식으로 암호화를 진행했습니다. 이는 AES 암호화 알고리즘을 CBC 모드로 사용하고, PKCS5Padding을 적용하여 데이터를 암호화하는 방식을 나타냅니다.
또한 유저 데이터의 경우 저장할때는 평문을 암호화하여 저장하고 조회할때는 암호문을 평문으로 복호화하여 조회합니다. 따라서 이를 Converter를 적용하여 자동으로 저장 및 조회시에 암 / 복호화가 가능하도록 구현했습니다.
다음은 간단하게 사용한 용어들을 정리해도록 하겠습니다.
용어 | 설명 |
IV | IV는 무작위로 생성된 고유한 값으로, 암호화 알고리즘에 입력되는 첫 번째 블록에 적용됩니다. IV의 크기는 암호화 알고리즘에 따라 다를 수 있지만, 일반적으로 블록 크기와 동일한 크기를 가지며, 보통 128비트 또는 16바이트입니다. |
CBC | CBC (Cipher Block Chaining)는 AES에서 사용되는 암호화 모드 중 하나입니다. CBC 모드는 블록 단위로 데이터를 암호화하기 전에 이전 블록의 암호문과 XOR 연산을 수행하여 암호문의 무작위성을 높이는 것입니다. |
PKCS5Padding | PKCS5Padding은 데이터 블록의 크기를 일정한 크기로 맞추기 위해 사용되는 패딩 방식입니다. 패딩은 데이터 블록의 크기가 암호화 알고리즘의 블록 크기와 일치하지 않을 때 추가되는 비트나 바이트입니다. PKCS5Padding은 PKCS #5라는 표준에서 정의된 패딩 방식으로, 블록 크기와 일치하지 않는 바이트를 추가하여 데이터를 패딩합니다. |
※ DB converter 적용
Converter는 JPA의 AttributeConverter 인터페이스를 구현하여 정의됩니다. AttributeConverter는 JPA 엔티티 클래스의 필드 타입과 데이터베이스의 컬럼 타입 간의 변환을 담당합니다. 사용 방법은
1. AttributeConverter 인터페이스를 구현한 Converter 클래스를 작성합니다.
2. Converter 클래스에서 convertToDatabaseColumn 메서드를 오버라이드하여 자바 객체를 데이터베이스 컬럼 값으로 변환하는 로직을 구현합니다.
3. Converter 클래스에서 convertToEntityAttribute 메서드를 오버라이드하여 데이터베이스 컬럼 값을 자바 객체로 변환하는 로직. 을 구현합니다.
4. Converter를 JPA 엔티티 클래스의 필드에 @Convert 어노테이션을 사용하여 등록합니다.
이렇게 등록된 Converter는 JPA가 엔티티와 데이터베이스 간의 변환 작업을 수행할 때 자동으로 적용됩니다. Converter를 사용하면 자바 객체와 데이터베이스 간의 복잡한 변환 작업을 간편하게 처리할 수 있습니다. 아래 사진은 구현한 converter 클래스와 이를 적용한 예시입니다.
* AttributeConverter 인터페이스를 구현한 Converter 클래스를 작성
보시는 바와 같이 DB에 저장될때는 encryptService에서 구현한 encryptedMemberData 메서드를 사용해 암호화 처리를 진행하고 DB에 저장하고 DB에서 조회될때는 encryptService에서 구현한 decryptedMemberData 메서드를 사용해 복호화 처리를 진행하는 것을 확인할 수 있습니다.
* 구현된 converter 적용
보시는 바와 같이 Entity 클래스 내에서 구현한 converter를 적용할 필드에 @Convert 어노테이션과 함께 converter를 정의하여 적용합니다. 이렇게하면 최종적으로 저희가 원하는 암 / 복호화 처리를 자동으로 진행할 수 있습니다.
이렇게해서 데이터 암호화 처리를 구현하였습니다. 원래는 이런걸 생각하지 않고 기능 구현에만 집중을 했었는데, 구현을 하다보니 결국 기능 구현은 기본적인 CRUD 밖에 안되고 이렇게 실제 사용자들은 보이지 않지만 서버 내부적으로 처리해야되는 필수적인 요소들을 구현하는 것이 더 중요하다는 것을 느꼈습니다. 실제 데이터 암호화는 보안에서 기본중에 기본이기 때문입니다.
이번을 계기로 전
DB에 저장하는 데이터는 개발자조차 쉽게 알 수 없게 해야한다.
를 적용하여 개발을 진행해야겠다고 생각했습니다!
'Study > Spring' 카테고리의 다른 글
조회 성능 향상을 위한 캐시 처리 With Redis (1) | 2024.01.29 |
---|---|
@Transactional 잘 모르고 쓰면 오히려 독이 될 수 있다. (0) | 2024.01.09 |
외부 API 사용 :: 마지막까지 깔끔하게 (2) | 2023.12.30 |
Spring Mail 인증 비동기 처리 & 비동기 retry 정책 적용 (1) | 2023.12.17 |
HttpMessageConverter vs ObjectMapper 비교 (0) | 2023.04.13 |