패스워드 저장 시 단방향 해시 함수의 문제점과 해결법
단방향 해시 함수(one-way hash function)의 다이제스트(digest)
보통 단방향 해시 함수는 수학적인 연산을 통해 원본 메시지를 변환하여 암호화된 메시지인 다이제스트를 생성한다.
원본 메시를 안다 -> 암호화된 메시지를 알 수 있다.
암호화된 메시지를 안다 -> 원본 메시지를 모른다.
한쪽으로만 알 수 있으므로 이와 같은 처리를 '단방향성'이라고 한다.
단방향 해시 함수는 예를 들어 비밀번호가 asd일 때,
해시 알고리즘인 SHA-256을 이용하여 암호화할 때, f52bvs32c2f3f86kk88vc6c490628285a482af15ascb19541d54saz526a3f6c7 와 같이 나온다면
asd1을 암호화하면 fb8c2e2b85ca81eb4350199faddd983cb26af3064614e737ea9f479621cfa57a 와 같이 나온다.
즉, 완전히 다른 암호문이 만들어진다. 하지만 SHA-256과 같은 해시 함수의 문제점은 남아있다.
단방향 해시 함수의 문제점
인식 가능성(recognizability)
a을 암호화하면 aasd2123zsg453513dfs ... 와 같이 만들어지고 디비에 저장된 aasd2123zsg453513dfs ... 와 같은지 비교할 것이다. 즉, 암호화된 상태로 비교할 것이다.
그렇다면 공격자가 미리 여러 개의 비밀번호를 암호화하고 탈취한 다이제스트(암호화된 비밀번호)와 비교한다면 원본 비밀번호를 찾을 것이다.
이와 같이 다이제스트 목록을 레인보우 테이블(rainbow table)이라 하고, 이와 같은 공격 방식을 레인보우 공격(rainbow attack)이라 한다. 게다가 다른 사용자의 패스워드가 같으면 다이제스트도 같으므로 한꺼번에 모두 정보가 탈취될 수 있다.
속도(speed)
해시 함수는 암호학에서 널리 사용되지만 원래 패스워드를 저장하기 위해서 설계된 것이 아니라 짧은 시간에 데이터를 검색하기 위해 설계된 것이다. 여기서 문제가 발견된다!🥺😬
해시 함수의 빠른 처리 속도로 인해 공격자는 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있다. 오히려 공격자에게 유리한 함수인 것이다.
MD5를 사용한 경우 일반적인 장비를 이용하여 1초당 56억 개의 다이제스트를 대입할 수 있다고 한다.
단방향 해시 함수 보완하기
솔팅(salting)
패스워드에 임의의 문자를 덧붙여서 암호화한다고 생각하면 된다. 그러면 공격자가 패스워드를 알아낸다고 하더라도 솔트(salt)를 알지 못하므로 원본 패스워드를 구하지 못할 것이다. 패스워드 일치 여부는 솔트와 패스워드의 다이제스트를 데이터베이스에 저장하고, 사용자가 로그인할 때 입력한 패스워드를 해시하여 일치 여부를 확인할 수 있다. 이 방법을 사용할 때에는 모든 패스워드가 고유의 솔트를 갖고 솔트의 길이는 32바이트 이상이어야 솔트와 다이제스트를 추측하기 어렵다.
Adaptive Key Derivation Functions
bcrypt
bcrypt는 애초부터 패스워드 저장을 목적으로 설계되고 현재까지 사용되는 가장 강력한 해시 메커니즘 중 하나이다. bcrypt는 보안에 집착하기로 유명한 OpenBSD에서 기본 암호 인증 메커니즘으로 사용되고 있고 미래에 PBKDF2보다 더 경쟁력이 있다고 여겨진다.
Bcrypt는 패스워드를 해싱할 때 내부적으로 랜덤 한 솔트를 생성하기 때문에 같은 문자열에 대해서 다른 인코드 된 결과를 반환한다. - 인식 가능성 해결
위 간단한 예시처럼 Bcrypt.hashpw()에 평문 패스워드와 BCrypt에서 제공해주는 솔트를 넣어서 암호화해주면, checkpw() 메서드를 통해서 간단하게 확인할 수 있다.
gensalt() 내부에서 보이듯이 salt()를 랜덤으로 만들어준다.
checkpw()를 보면 해싱된 비밀번호를 바이트화 시키고, 평문과 해싱된 비밀번호를 salt로 하여금 다시 해싱 처리를 한 후, 바이트화 시켜서 비교한다.
이 외
키 스트레칭(key stretching)
입력한 패스워드의 다이제스트를 생성하고, 생성된 다이제스트를 입력 값으로 하여 다이제스트를 생성하고, 또 이를 반복하는 방법으로 다이제스트를 생성할 수도 있다. 이렇게 하면 입력한 패스워드를 동일한 횟수만큼 해시해야만 입력한 패스워드의 일치 여부를 확인하는 방법이다.
PBKDF2
가장 많이 사용되는 key derivation function은 PBKDF2(Password-Based Key Derivation Function)이다. 해시 함수의 컨테이너인 PBKDF2는 솔트를 적용한 후 해시 함수의 반복 횟수를 임의로 선택할 수 있다.
scrypt
scrypt는 PBKDF2와 유사한 adaptive key derivation function이다. scrypt는 다이제스트를 생성할 때 메모리 오버헤드를 갖도록 설계되어, brute-force attack(무지성 대입)을 시도할 때 병렬화 처리가 매우 어렵다. 따라서 PBKDF2보다 안전하다고 평가되며 미래에 bcrypt에 비해 더 경쟁력이 있다고 여겨진다. scrypt는 여러 프로그래밍 언어의 라이브러리로 제공받을 수 있다.
마치며
MD5, SHA-1, SHA-256, SHA-512 등의 해시 함수는 메시지 인증과 무결성 체크를 위한 것이다. 이것을 패스워드 인증을 위해 사용하면 앞에서 말한 인식 가능성과 빠른 처리 속도에 기인하는 취약점이 존재한다. 매우 강력한 패스워드 다이제스트를 생성하는 시스템을 쉽게 구현하고 싶다면 bcrypt를 사용하는 것이 좋다. 대부분의 프로그래밍 언어에서 라이브러리를 사용할 수 있고, 검색 엔진에서 "bcrypt <프로그래밍 언어>"로 검색하면 쉽게 예제를 구할 수 있다.
패스워드 다이제스트의 강도는 결국 패스워드 자체의 길이와 유일성 같은 엔트로피에 의해서 결정된다. 즉, 패스워드를 길게 설정하고 모든 사이트마다 다른 패스워드를 설정하는 게 최선이다. (이론적으로는)