발전하는 춘배

Database에 패스워드를 저장하는 방법(Hash와 Salt, Pepper) 본문

backend/보안

Database에 패스워드를 저장하는 방법(Hash와 Salt, Pepper)

춘배0 2024. 8. 28. 00:33

개요

프로젝트를 진행하던 중 유저의 패스워드를 어떻게 DB에 저장해야 좋을 지 고민하게 되었고, 이 과정에서 알게 된 것들을 정리해 본다.


최악

최악이다

가장 최악의 방법은 위와 같이 유저의 password를 문자 그대로 저장하는 것이다.
유저의 패스워드를 이렇게 저장하는 서비스는 존재해선 안 된다.


Hashing

SHA512로 해싱하여 저장

적어도 패스워드는 이렇게 암호화해서 DB에 저장해야 하는데, 이 때 해시 알고리즘을 사용한다.
해시 알고리즘에는 여러 종류가 있는데, 위의 예시는 SHA512 알고리즘을 이용하였다.
해시 알고리즘들이 가지는 특성은 다음과 같다.

  • 입력값을 i, 알고리즘을 거쳐 나온 출력값을 o라 하자.
  • i가 주어지면 o를 쉽게 구할 수 있다.
  • 같은 i에 대해서는 항상 같은 o가 나온다.
  • i가 한 글자만 달라져도 o는 매우 크게 변한다.
  • i의 길이에 상관 없이 o는 항상 고정된 길이를 갖는다. (SHA512의 경우 항상 512비트=64바이트의 o가 나온다)
  • o가 주어진 경우 i를 구하는 데 매우 오랜 시간이 소요된다. 즉 역추론이 힘들다.

여기서 역추론이 힘들다는 점에 의해서 hashing은 효과적인 패스워드 저장 방법이 되겠다.

즉 DB가 털려도 DB에는 해시된 값만 저장이 되어 있기 때문에, (이론상) 해커는 유저의 실제 패스워드 `qwer1234`를 알 수 없다.

그러나..


해킹

해시 알고리즘의 특성 중, 같은 i에 대해서는 항상 같은 o가 나온다는 특성에 주목해보자.
SHA512 등의 해시 알고리즘들은 누구나 쉽게 접할 수 있다. 해시값 o를 계산해주는 사이트들도 정말 많다.
해커들은 이걸 이용해서 어떤 i가 어떤 o와 대응되는지 미리 계산해 표를 만들 수 있다.

궁극의 표

이런 식으로 모든 i에 각각 대응하는 o를 표를 만들어 놓으면, o에서 `bd877cf94f1a6312…`를 찾아 원래 i가 `qwer1234`라는 걸 추론해낼 수 있다.
물론 이렇게 모든 대응을 저장하는 무식한 테이블은 그 용량이 매우 커서 현실적으로는 불가능하지만, 레인보우 테이블이라는 것을 통해 계산 시간을 약간 희생해서 공간을 엄청나게 줄일 수도 있다.
만약 이런 테이블이 없더라도, 유저 kim이 개인정보 관리에 소홀해서 자신의 원래 패스워드가 `qwer1234`라는 것이 공개된 경우, 해커는 `qwer1234` ← `bd877cf94f1a6312…`라는 정보를 알게 된다. 그러면 단지 kim과 우연히 패스워드가 같았다는 이유만으로 아무 죄 없는 lee의 원래 패스워드도 노출되게 되는 것이다.
아무튼 서버 개발자 입장에서는 lee처럼 억울한 유저가 나오지 않도록 패스워드를 저장해야 하는데, 대표적인 방법이 salt, 소금이다.


Salt

소금

이런 식으로 각 유저마다 랜덤한 salt를 부여해서, 패스워드를 해싱할 때 기존 패스워드+salt의 문자열을 해싱해서 저장하는 것이다.
예를 들어 kim의 해시값은 `qwer12341sda`를 해싱해서 나온 것이고, lee의 해시값은 `qwer1234fh2k`를 해싱해서 나온 것이다.
이렇게 하면 kim의 `qwer1234`가 노출되어도 lee에게 저장된 해시값이 kim의 해시값과는 다르기 때문에 DB만 털린 경우 kim과 lee의 원래 패스워드가 같았다는 사실을 해커가 알 수 없다.
쉽게 말해, salt의 목적 중 하나는 동일한 패스워드에 대해 다른 해시값을 생성하는 것이다.
서버 입장에서 salt를 도입한다고 해서 연산 시간이 크게 늘어나는 것은 아니다. 다시 말하지만 같은 i에 대해서는 항상 같은 o가 나오기 때문에 서버 입장에서 로그인을 처리할 때 올바른 패스워드인지 확인하는 것은, 단순히 입력받은 패스워드에 salt를 더해서 해시 함수를 돌려 저장된 값과 같은지만 확인하면 되기 때문이다. 따라서 DB 용량이 조금 커진다는 것을 제외하고는 서버 입장에서 salt를 도입하지 않을 이유가 없다.


의문과 해결

의문

여기서 나는 의문이 생겼다.
만약 해커가 레인보우 테이블을 가지고 있어서 `qwer12341sda`와 `qwer1234fh2k`에 해당하는 해시값을 모두 알고 있다면, DB가 노출된 경우 salt가 각각 `1sda`와 `fh2k`라는 사실도 함께 노출된 상태일 텐데, 그렇다면 원래 비밀번호를 추론하는 건 매우 쉬운 일을 것이다. 즉 salt가 정말로 의미가 있는 것인지 의문이 들었다.

해결

결론적으로 salt는 의미가 있다.

  1. 위에서도 언급했지만 salt의 한 가지 목적은 같은 패스워드에 대해 서로 다른 해시값을 만들어내는 것이다. 이는 salt를 유저마다 랜덤하게 부여하기만 하면 된다.
  2. 확률적으로 접근해보자. 궁극의 테이블은 가능한 input 길이가 한 글자만 늘어나도 전체 테이블 용량은 기하급수적으로 증가한다. 즉, `qwer12341sda`와 `qwer1234fh2k`를 모두 알고 있는 테이블의 용량은, salt를 적용하지 않아 `qwer1234`만 알고 있어도 해킹이 가능한 테이블의 용량보다 훨씬 클 것이다. 즉 salt를 적용하면 해커가 궁극의 테이블을 가지고 있을 확률이 salt를 적용하기 전보다 훨씬 낮아진다.

Pepper

나의 의문에 대해 확률적으로가 아니라 근본적으로 해결하는 방법이 있다. 바로 pepper, 후추이다.
별 건 아니고, 소금처럼 DB에 함께 저장하는 대신 이 소금을 코드 내에 하드코딩하거나 다른 DB로 빼내는 것이다.
salt는 앞서 언급한 대로 DB만 털리면 어쨌든 해커가 원래 패스워드를 추론해 낼 가능성이 존재한다.
그러나 pepper를 사용하면 단순히 pw의 해시값만 저장되어 있는 DB만 털릴 경우 해커는 이 후추 값을 모르기 때문에 궁극의 테이블을 사용한다 하더라도 원래의 패스워드를 추론할 수 없다.


참고 자료

SHA512 Hash 온라인 툴

전체적인 설명