김태오

salting, hashing 에 대한 이해와 주의점 본문

Security

salting, hashing 에 대한 이해와 주의점

ystc1247 2024. 4. 18. 19:00

유저 정보 시스템을 구성할 때, 가장 중요한 요소는 유저 비밀번호의 보호이다. 

 

이 때 사용하는 것이 salting 이라는 기법인데, 어원은 https://stackoverflow.com/questions/244903/why-is-a-password-salt-called-a-salt 라고한다.

딱히 그럴듯한 답은 없는데, foo - bar 등 흔히 쓰이는 용어들의 어원이 골때릴 때가 있다.

 

일단 해싱이라는 용어부터 짚고 가야 하는데, 요약하자면 문자열을 다른 문자열로 바꾸는 “단방향” 프로세스이다. 단방향이라 하면 해싱된 문자열을 원 문자열로 되돌릴 수 없음을 말한다.

반대로 양방향 암호화에는 key 등 암호화와 복호화 양쪽에서 공통으로 들고 있는 원 문자열과 바뀐 문자열을 유추할 수 있는 방법이 있다.

 

해싱에는 갖가지 알고리즘이 사용된다. Phpass, libsodium, sha256, sha512 등이 있는데 안전하고 공인된 알고리즘이 무엇인지는 찾아보도록 하자.

MD5, SHA1, SH2 등은 Merkle–Damgård construction 라는걸 사용하는데, 이건 length extension attack 에 취약하니 쓰지 말도록 하자.

 

우선 비밀번호를 사용한 인증 방식의 절차에 대해 알아보고자 한다.

 

  1. 유저는 계정을 생성한다.
  2. 비밀번호는 해싱 이후 DB 에 저장된다. 
  3. 유저가 로그인하려 할 때, 유저가 입력한 비밀번호에 해싱 알고리즘을 사용하여 암호화한 뒤 DB 의 비밀번호와 대조한다.
  4. 해시가 일치할 경우 통과

 

대부분의 로그인 인증에서 4의 과정이 실패할 때 아이디가 틀렸는지, 비밀번호가 틀렸는지 알려주지 않는데, 이는 화나게 하려는 의도가 아닌 username 이 맞을 때의 password brute force 를 막기 위한 작은 보안 절차이다.

 

해싱 절차를 거친 보안이 안전할까? 당연히 그렇지 않다.

 

서비스에서 어떤 해싱 알고리즘을 사용하는지 파악이 되면, 유저 데이터베이스가 털린 후 password 까지 유추하는 과정이 어려울 리 없다.

 

만약 해싱 알고리즘을 알고 있지 않더라도, 해싱 알고리즘을 brute force 하는 방법으로 비밀번호를 알아낼 수도 있고, breacher 의 진심의 농도에 따라 lookup table, rainbow table 이라는걸 사용할 수도 있다.

https://en.wikipedia.org/wiki/Rainbow_table#:~:text=A%20rainbow%20table%20is%20a,form%2C%20but%20as%20hash%20values. 

요약하자면 해싱 알고리즘에 따라 해싱된 모든 문자열을 원 문자열로 되돌리는 key-value 형식의 테이블인데, 자연히 테이블 사이즈가 개크다. 

복잡한 해싱 알고리즘들은 각각의 character 가 어떤 문자열로 치환되는 것이 아닌 character 들에 맞물려 있는 복잡한 형식의 암호화가 이루어지기에 “abcdef” 와 “abcdee” 의 암호화는 천지 차이가 나기 때문이다.

 

그래서 등장하는게 salt 이다. 

 

Lookup table 과 rainbow table 의 동작 원리는 하나의 문자열이 정확히 다른 하나의 문자열로 치환되기 때문이다.

예를 들어 “password” 라는 비밀번호가 있다고 치자.

한 서비스에서 “password”를 암호화할 때, 아무리 복잡한 해싱 알고리즘을 쓴다 한들 모든 암호화된 “password” 들은 “password”에 대응된다. 이렇기에 해싱을 하기 전 “password” 에 특별한 문자열을 append 하여 그것을 해싱하는 것이며, 이 문자열을 salt 라 하는 것이다.

 

 

Salt 를 사용하는 것에도 여러 주의점이 있다.

 

우선 salt 를 재활용하는 것이다. Salt 의 uniqueness 를 해치는 순간 salt 를 문자열에 더하기 전 해싱의 보안 상태와 별 다를 부분이 없다. 공격자가 salt 가 무엇인지 파악이 된 후, 그저 salt 를 비밀번호에 append 하여 reverse lookup table 을 사용하면 된다.

Salt 는 각각의 유저 별로 반드시 달라야 한다. 이에는 언어에서 지원하는 random function 을 사용하여 각각의 character 를 생성할 수도 있지만, 그보다 안전한 CSPRNG 을 사용하자.

https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator 원리는 잘 모르겠지만 암호학적으로 더 안전한 랜덤 함수이다. CPRING 도 대부분의 언어별로 라이브러리가 존재한다. 예를 들어 Java 의 java.security.SecureRandom 과 Python 의 secrets

 

다음으로 작은 salt 문자열 크기이다. 예를 들어 salt 가 고작 4개의 ASCII 문자열을 사용한다면, 95^4 = 81450625 개의 salt 가 생성된다. 많아 보일지 몰라도, 현대 컴퓨터의 속도와 저장 공간의 용이성을 생각해 보았을 때 턱없이 부족한 숫자이다.

전문가들의 소견으로는 salt 가 더해진 후 해싱된 문자열 - 예컨데 SHA256 으로 해싱된 문자열 (256bits == 32bytes == 64 characters) 의 크기와 같거나 커야 된다고 한다. SHA512 를 사용하면 salt 의 최소 크기는 128 char 이라는 것이다.

 

이제 salt 가 붙은 문자열을 해싱할 차례이다. 

 

SHA256, SHA512, WHIRLPOOL 등의 빠르고 안전한(안전하다고 여겨졌던) 알고리즘들이 존재하고, 이보다 조금 느리지만 현대 보안에 맞춰가는 PBKDF2, bcrypt, scrypt 등의 알고리즘이 있다.

오버엔지니어링일 수 있으니 서비스의 깜냥에 맞게 적절히 선택하자.

https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:scrypt/scrypt.go script 의 go library 소스코드인데, 개복잡한걸 보니 확실히 안전해 보인다.

 

다음은 python 으로 짠 salt 만들기에서 password hashing 까지의 짧은 함수이다. 역시 짧은 함수는? 파이썬

 

class EncryptPassword:

    @staticmethod

    def salt_password(password):

        salt = os.urandom(64) # salt 생성

        salt_hex = salt.hex() # 저장을 위한 salt > hex 로 변환

		# PBKDF2 (SHA-512 의 100,000 반복) 으로 해싱
        dk = hashlib.pbkdf2_hmac('sha512', password.encode(), salt, 100000, dklen=64)

        hashed_password_hex = dk.hex() # 저장을 위한 비밀번호 > hex 로 변환

        return hashed_password_hex, salt_hex