문제의 시작

Password 저장은 단순히 문자열을 암호화하는 문제가 아니다. 사용자가 같은 비밀번호를 쓰더라도 저장된 값은 달라야 하고, DB가 유출되더라도 원문을 되돌리기 어려워야 한다. 이 글은 hashing과 salting을 보안 경계로 이해하기 위해 정리한 기록이다.

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

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

명확한 답이 있는 것은 아니지만, foobar처럼 흔히 쓰이는 용어들의 어원이 의외로 독특한 경우가 있다.

먼저 hashing이라는 용어부터 정리해야 한다. 요약하면 문자열을 다른 문자열로 바꾸는 “단방향” process다. 단방향이라는 말은 hashing된 문자열을 원 문자열로 되돌릴 수 없다는 뜻이다.

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

hashing에는 여러 algorithm이 사용된다. Phpass, libsodium, sha256, sha512 등이 있지만, 실제 서비스에서는 현재 안전하다고 검증된 algorithm을 기준으로 선택해야 한다.

MD5, SHA1, SH2 등은 Merkle-Damgard 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.

결과를 어떻게 검증했는가

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

요약하면 hashing algorithm별로 hash 값을 원 문자열에 대응시키는 key-value 형태의 table이다. 가능한 조합을 담아야 하므로 table size가 매우 커진다.

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

그래서 등장하는 개념이 salt다.

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

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

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

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

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는 salt가 더해진 후 hashing된 문자열, 예를 들어 SHA256 hash 값(256bits == 32bytes == 64 characters)의 크기와 같거나 그보다 크게 잡는 편이 좋다고 한다. SHA512를 사용하면 salt의 최소 크기를 128 characters 수준으로 보는 식이다.

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

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

서비스의 보안 요구사항과 운영 비용에 맞춰 적절한 algorithm을 선택해야 한다.

https://cs.opensource.google/go/x/crypto/+/refs/tags/v0.22.0:scrypt/scrypt.go scrypt의 Go library source code다. 구현이 복잡한 만큼, password hashing algorithm이 단순한 문자열 변환 이상의 비용을 의도적으로 만든다는 점을 확인할 수 있다.

다음은 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

보안 기준

Hash와 salt를 안다는 것과 안전한 password 저장을 설계할 수 있다는 것은 다르다. 알고리즘 선택, salt 생성, work factor, 검증 방식, migration 전략까지 함께 봐야 한다. 보안 기능은 작동 여부보다 공격자가 어떤 정보를 얻는지 기준으로 판단해야 한다.