Spring

[Spring Security] Bcrypt의 salt는 어디에 저장될까?

jhkimmm 2022. 1. 31. 21:50

Spring Security의 PasswordEncoder를 공부하며 든 궁금증을 정리합니다.

 

암호화 해시함수는 단방향 알고리즘이기 때문에 해시값으로 저장된 비밀번호를 역으로 계산해서 원래의 암호를 알아내는 것은 불가능하며, 로그인을 할때는 입력받은 값을 같은 해시함수에 넣어 결과값을 얻고 이 값과 같은 값이 데이터베이스에 있는지 확인함으로써 로그인을 처리합니다.

 

하지만 이런 방식 또한 취약점이 있는데 바로 입력 가능한 모든 문자열 조합을 해시함수에 넣어서 결과를 저장한 테이블인 Rainbow Table을 사용하여, 탈취한 암호값을 일일이 대조하여 비밀번호를 알아내는 방식의 브루트 포스 공격이 가능하다는 점 입니다.

 

그래서 현대의 암호화 해시함수에서는 입력값에 랜덤으로 생성한 값(이를 salt라고 합니다.)을 더해서 한번에 해시함수에 넣음으로써 Rainbow Table을 활용한 브루트 포스 공격을 막습니다.

 

Spring Security에서는 유저의 패스워드를 암호화하여 저장하기 위한 default PasswordEncoder로 BCryptPasswordEncoder를 사용하며, 스프링에서 ID와 비밀번호만 입력받아 회원가입을 진행하는 간단한 서비스를 구현하면 아래와 같습니다.

여기서 저는 궁금증이 들었습니다.

 

분명 암호화 해시함수는 Rainbow Table을 활용한 브루트 포스 공격을 막기 위해서 입력받은 패스워드에다가 랜덤하게 생성된 salt를 더해서 해싱한 값을 데이터베이스에 저장하므로, 로그인을 진행할 때도 회원가입할 때 사용했던 salt와 똑같은 salt를 입력받은 패스워드에 더해서 해싱해줘야 하는데 저는 어디에서도 회원가입시 암호화에 사용된 salt값을 저장해두지 않았습니다. 

그렇다면 로그인할 때 도대체 어떻게 알고 회원가입 때 사용한 salt값을 가져오는 걸까요?

저는 혹시 스프링 내부에서 제가 모르는 어딘가에 스스로 salt를 저장하는 디비를 만들고 거기에 저장하는 건 아닐까 생각이 들어서 BCryptPasswordEncoder의 코드를 들여다 봤습니다.

BCryptPasswordEndoer.java 파일

암호화를 진행하는 encode 메서드와 salt값을 가져오는 getSalt 메서드만 보일 뿐 딱히 salt를 저장하는 코드는 보이지 않았습니다. 혹시나 BCrypt.gensalt()와 BCrypt.hashpw() 에서 저장하는 코드가 있을까 싶어서 한단계 더 들어가 보았으나

BCrypt.java 파일

여기서도 어딘가에 salt값을 저장하는 것 같아 보이는 부분은 찾을 수 없었습니다.

조사해본 결과 허무하게도 salt값은 해시값에 이어붙여서 함께 저장되고 있었습니다.

$2a$10$vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa

bcrypt는 위와 같은 형태로 해시값을 생성하는데 이는 사실 '$'로 구분되는 3가지의 필드라고 볼 수 있습니다.

  • '2a'는 사용된 bcrypt알고리즘의 버전을 의미합니다.
  • '10'는 cost factor입니다. cost factor가 10이라는 의미는 key derivation 함수가 2^10번 반복 실행된다는 의미입니다. 이 값이 커질 수록 해시값을 구하는 속도가 느려집니다.
  • 'vI8aWBnW3fID.ZQ4/zo1G.q1lRps.9cGLcZEiGDMVr5yUP1KUOYTa'가 바로 salt값과 암호화된 비밀번호값 입니다. 첫 22개의 문자가 16바이트의 salt값으로 디코딩 됩니다.

bcrypt는 이런 방식으로 salt값을 해시값에 붙여서 같이 저장합니다.

 

여기서 다시한번 생긴 궁금증은 "salt값을 이렇게 쉽게 노출시켜도 되는가" 입니다. salt값이 이런식으로 노출되어 있으면 해커가 탈취한 salt값을 적용해서 다시 Rainbow Table을 만들어서 브루트 포스를 시도하면 그만 이므로 salt값 또한 어딘가에 비밀리에 저장해야 하지 않나 하는 생각이 들었습니다. (애초에 저는 salt값이 노출되면 안된다는 근거없는 뇌피셜 덕분에 salt가 해시값과 같이 저장된다는 생각은 하지도 못하고 salt가 저장된 위치를 찾아 헤멨습니다...)

결론

결론은 "굳이 salt값을 Secret하게 저장할 필요가 없다" 입니다.

그 이유는 다음과 같습니다.

 

첫번째, 우리는 Secret을 완벽하게 보장할 수 없습니다.

우리가 secret을 완벽히 보장할 수 있다면 굳이 암호화할 필요도 없이 평문을 그냥 디비에 저장해버리면 그만입니다. 비밀번호 인증 체계는 공격자가 우리가 아는 모든 정보를 알고 있다는 가정하에 출발합니다.

 

두번째, salt의 목적과 상관이 없습니다.

salt의 목적은 이미 계산되어 정의된 Rainbow Table을 사용하지 못하게 하는데에 있습니다. salt값을 사용한다는 자체가 이미 정의된 Rainbow Table을 무용지물로 만드는 것이기 때문에 목적을 충분히 달성할 수 있습니다.

 

세번째, salt값이 노출되어도 Rainbow Table을 새로 만들기는 어렵습니다.

일단 Rainbow Table자체가 새로 만드는데 엄청난 시간과 리소스가 들어갑니다. 최대 8자리 영문 대소문자, 숫자를 충족하는 입력값이라면 사전 텍스트 파일만 해도 대략 1663GB이고, 10자리 영문 소문자+숫자를 충족하는 입력값이라면 초당 1억번 대입을 한다 하더라도 1년 이상 걸린다고 합니다.

게다가 bcrypt는 해시함수를 계산하는 것 자체가 기본적으로 시간이 오래 걸리도록 설계되어 있으며(일반적으로 약 1초정도 걸린다고 합니다.), 이마저도 cost factor의 값을 늘리면 걸리는 시간을 더 증가 시킬 수 있습니다. 공격자가 정말로 엄청난 컴퓨팅 리소스를 가지고 있더라도 이정도의 시간 지연이라면 공격을 인지하고 충분히 대응하고도 남을 시간이라고 할 수 있습니다.(현실적으로 Rainbow Table제작이 불가능 합니다.)

 

스프링 시큐리티의 PasswardEncoder는 salt를 어디에 저장하는가 하는 궁금증에서 시작해서 정말 멀리 왔는데요. 이정도면 PasswardEncoder 충분히 믿고 사용할 수 있을 것 같습니다!

 

[참고]

 

How can bcrypt have built-in salts?

Coda Hale's article "How To Safely Store a Password" claims that: bcrypt has salts built-in to prevent rainbow table attacks. He cites this paper, which says that in OpenBSD's implement...

stackoverflow.com