비밀번호 암호화 - BCryptPasswordEncoder

HYK·2022년 11월 15일
0

project

목록 보기
1/8
post-custom-banner

개요

프로젝트를 시작하면서 여러 가지 이슈들이 생긴다.
그중에 최근에 만났던 이슈인 비밀번호 암호화에 대해서 이야기해 보려고 한다.

User 로그인 회원가입 등의 기능을 구현 중에 User의 중요 정보인 비밀번호를 암호화해서 DB에 저장해야 할 일이 생겼다.

암호화 테스트 도중 해싱 한 값은 항상 다른데 matches 메서드는 항상 정확하게 동작하는 걸 보고 솔트 값이 BCryptPasswordEncoder 클래스 내부에 저장되는 게 아닌가? 하는 의심이 들기 시작했고 여러 가지 테스트를 해보았다.

BCryptPasswordEncoder로 한번 해싱 한 다이제스트는 프로그램을 껐다 킨 후에 matches를 돌리거나
BCryptPasswordEncoder 객체를 여러 개 생성해서
1번 객체로 해싱하고 2번 객체로 matches를 돌려도 항상 정확한 값이 나왔다.

BCryptPasswordEncoder는 도대체 솔트 값을 어디에 어떻게 저장해두길래 프로그램을 종료하거나 여러 객체를 만들어서 사용해도 잘 동작하는지 궁금했다.

구현을 하는 데만 관심을 둬서 그런지 정작 암호화 과정이나 안전성에 대해서는 관심을 가진 적이 없었기 때문에 이번 기회에 어떤 식으로 암호화가 되는지 알아보고 그 외에 비밀번호를 해싱하고 암호화하는 방법은 어떤 게 있고 어떤 기술들을 이용하고 있는지 한번 확인해 보자.


1. 암호화란?

암호화 기법 중에서 암호화는 양방향 기법에 속한다.
즉 암호화/복호화가 가능해서 암호문을 평문으로 바꿀 수도 있고 평문을 암호문으로 바꿀 수 있는 특징이 있기 때문에 양방향 기법이라고 한다. 또한 이런 특징을 이용해서 보통은 송수신자가 있는 통신에서 주로 사용된다.


2. 해싱이란?

암호화 기법 중에서 해싱은 단방향 기법에 속한다.
암호화와는 반대로 난독화된 암호문(다이제스트)을 평문으로 바꿀 수 없다. 따라서 해싱은 통신보다는 데이터 암호화를 할 때 많이 사용된다. 또 다른 특징으로는 같은 입력에 대해 고정된 길이와 같은 값을 출력하는 특징이 있다.

여기서 우리가 자세히 알아볼 것은 해싱이다. BCryptPasswordEncoder은 해싱을 이용하고 비밀번호를 암호화하는 것은 비밀번호 즉 데이터를 암호화하는 것이기 때문에 해싱으로 암호화하는 게 조금 더 적절하다고 할 수 있다.


해싱은 안전한가?

과연 해시 함수로 만들어낸 비밀번호는 보안에 안전할까?
해싱의 특징 중에 고정된 길이 그리고 같은 입력에 대해서는 같은 값을 반환하는 특징이 있는데 바로 이 특징이 해싱에 취약점이라고 할 수 있다.

왜? 취약할까?

예를 들어보자
내 비밀번호를 pass라고 저장했고 이를 해싱 해서 dj2d34ko 8자리의 다이제스트(암호문)가 생성됐다.
그리고 앞으로 모든 pass라는 문자는 해싱의 특징처럼 다음과 같이 dj2d34ko라는 다이제스트가 만들어질 것이다.

이를 통해서 원문과 다이제스트를 매칭해서 테이블을 만들 수 있는데 이것이 바로 레인보우 테이블이라는 것이다 레인보우 테이블은 해시 함수로 만들어낼 수 있는 값들을 대량 저장해둔 표다.

여기서 보통의 해시 함수는 속도가 매우 빨라서 레인보우 테이블과 함께 브루트 포스 공격을 시도하게 된다면
해킹할 다이제스트와 레인보우 테이블을 계속 비교하고 이 작업 속도가 매우 빠르게 진행되기 때문에 쉽게 다이제스트를 풀 수 있는 것이다.

이렇게 취약한 해싱을 어떻게 안전하게 쓸 수 있을까?
바로 솔트와 키스트레칭을 이용하면 해싱을 좀 더 안전하게 할 수 있다.


키스트레칭이란 ?

키스트레칭은 해싱을 여러 번 돌리는 작업을 말한다 이를 통해서 얻는 이 점이 뭐가 있을까?

pass -> 첫 번째 해싱 -> dj2d34ko -> 두 번째 해싱 -> sfsd324 -> ...
이처럼 우리가 처음에 진행했던 값과 다른 값이 나온다.

이것도 마찬가지로 같은 입력에는 같은 출력이 나오겠지만 처음에 다이제스트를 얻었던 시간 보다 훨씬 많은 시간이 소요된다 따라서 브루트 포스 공격을 막기 위한 방법이라고 할 수 있다.


솔트란?

솔트는 소금이다...

쉽게 생각해서 내 패스워드에 양념을 치는 느낌이라고 보면 될 것 같다.(양념을 쳐서 원래 원재료를 알아볼 수 없게 만드는 것 ?)

내 비밀번호를 그대로 해싱 하는 게 아니라 솔트라는 랜덤 한 문자열을 붙인 다음에 해싱 하는 것이다.
그러면 아까 우리가 우려한 레인보우 테이블을 만들 수 없게 된다 그 이유는 내가 pass라는 값을 해싱 해도 랜덤 한 솔트 값에 따라서 다이제스트가 계속 바뀌게 돼서 레인보우 테이블을 만들 수 없게 된다.


BCryptPasswordEncoder 내부

자 그럼 이제 BCryptPasswordEncoder의 내부는 어떤 식으로 구현되어 있는지 한번 간단하게 살펴보자

encode

  1. encode를 사용하면 이렇게 getSalt 함수를 불러와서 salt 값을 생성하고 hashpw 메서드를 실행한다.

  2. hashpw 내부에서 여러 가지 작업들을 거쳐서 해싱이 완료된다.

matches

  1. 맨 아래에 checkpw 메서드를 통해서 패스워드를 체크한다 첫 번째 인자는 rowpassword를 그리고
    두 번째 인자로는 암호화된 다이제스트를 넣어준다.

  1. checkpw 내부에 보면 값을 비교하는 메서드인 equals 인자로 첫 번째는 다이제스트를 두 번째 인자로는 hashpwforcheck라는 메서드의 리턴 값을 인자로 준다. 여기서는 원문과 다이제스트를 함께 넘겨준다.

  1. 그런데 hashpwforcheck에서는 두 번째 인자를 salt라는 이름으로 받는다 여기서부터 뭔가가 이상했다.
    다이제스트 자체를 왜 salt라고 했을까? 또 hashpw라는 encode 때 사용했던 똑같은 메서드를 사용하는데
    그때는 랜덤 한 salt 값을 줬는데 왜 matches에서 사용할 때는 다이제스트를 salt로 주는지 궁금했다.

  1. hashpw 메서드에 무언가가 있을까 해서 좀 더 자세히 들여다봤는데 충격적이게도 salt라는 이름으로 받은 다이제스트에서 추출해 내서 사용했다(real_salt).

그렇다 BCryptPasswordEncoder는 salt를 어딘가에 저장해두는 형태가 아니라 다이제스트에 salt 값을 붙여서 사용하는 것이었다.

즉 matches 메서드는 기존의 다이제스트에서 salt를 뽑아서 그 salt로 원문을 암호화한 후에 비교하는 것이다.

그런데 앞서 살펴본 솔트라는 키워드를 고민해 봤을 때 솔트를 알게 되면 결국 레인보우 테이블을 만들 수 있고 위험해지는 게 아닌가? 생각할 수 있다.


솔트 값을 외부로 유출해도 되는가?

여기서는 2가지의 관점에서 볼 수 있다.

  1. 만약 해시 함수 한 개에 솔트 값이 1개를 사용하고 비밀번호를 암호화해서 db에 저장해둔다면
    해당 해시 함수로 만든 모든 비밀번호들은 솔트 값을 해커가 취득하는 동시에 시간은 좀 걸릴 수 있지만
    레인보우 테이블을 만들어서 해킹할 수 있다. 따라서 이 경우에는 솔트 값을 잘 보관해야 하고 유출돼서는 안된다.

  2. BCryptPasswordEncoder와 같은 방식을 사용한다면?
    여기서는 솔트 값이 1개가 아니다 해싱 할 때 매번 랜덤 한 솔트가 만들어지기 때문에 1개의 비밀번호에 1개의 솔트를 사용한다고 볼 수 있다.
    따라서 1개의 비밀번호를 풀기 위해 해당 솔트로 레인보우 테이블을 만들어봤자 이 솔트를 사용한 1개의 비밀번호에 관해서만 유효하기 때문에 많은 리소스가 들어가는 레인보우 테이블을 굳이 만들어서 1개의 비밀번호에 대해서 사용하는 것은 해커의 입장에서는 가치가 없는 일이다.
    따라서 이 경우에는 솔트 값이 가지는 가치는 이전 예시에 비해서 그다지 중요하지 않다는 얘기이다.


profile
Test로 학습 하기
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 5월 6일

salt를 어떻게 저장해야 하는지 고민을 하고 있었는데 덕분에 방향을 잡게 되었습니다.
감사합니다. ^^

답글 달기