이 글은 이 하나의 질문에서 시작한다. 사용자로부터 입력받은 비밀번호를 안전하게 주고받을 것이며(HTTPS를 사용한), 건내온 비밀번호를 DB에 그대로 저장하면 안된다는 이야기를 들어본적은 있을 것이다.
이번 글에서는 건내받은 비밀번호를 어떻게 저장할 것이며, 이렇게 저장된 비밀번호는 안전한지에 대해 알아보자.
암호학에서, hash function은 다양한 크기의 data를 고정된 길이의 문자열로 바꾸는 함수를 의미한다. 이 함수의 input을 message 혹은 input이라 부르고, 함수의 실행 결과로 나온 고정된 길이의 문자열을 hash 혹은 message digest라고 한다. OWASP에 따르면, hash function은 다음의 주요한 속성을 가져야 한다:
따라서 암호화/복호화와 달리, hashing은 단방향(one-way)의 메커니즘이다. Hash가 된 데이터는 unhash 될 수 없어야 한다.
주로 사용되는 hashing algorithm에는 Message Digest(MDx) algorithm인 MD5가 있으며 Secure Hash Algorithms(SHA)인 SHA-1, SHA-2 family가 있다.
비트코인에서는, integrity와 block-chaining에 사용되는 hash function으로 SHA-256 algorithm을 사용한다.
Preimage attack은 암호학적 hash function을 공격하는 방식으로, 출력값이 같은 새로운 입력값을 찾는 해시 충돌 공격이다.
어떤 hash function들은 널리 알려져있지만 보안적인 것을 요구하지 않는 경우도 있다. 대표적으로 CRC(Cyclic Redundancy Check)라는 hash function이 있다. 이 hash function은 네트워크를 통해 전송된 데이터에 오류가 있는지 없는지 검사하는 checksum을 생성한다. 이 hash function은 preimage resistant하지 않기 때문에 보안용으로 사용하는 것은 적절하지 못하다.
따라서 cryptographic hash function(암호학적 해시함수)은 perimage resistant 해야한다. 그러기 위해서는 비가역적이어야 하고, input값의 아주 작은 변화도 output을 크게 바꾸어야 한다(예측할 수 없도록).
SHA-256를 사용하여 hash를 해보는 예시를 작성해보자. Node.js에 내장된 "crypto" library를 사용하였다:
const { createHash } = require("crypto");
const hash1 = createHash("sha256");
hash1.update("woo94");
console.log(`hash1 digest: ${hash1.digest("hex")}`);
const hash2 = createHash("sha256");
hash2.update("woo95");
console.log(`hash2 digest: ${hash2.digest("hex")}`);
hash1은 "woo94"라는 string 값을, hash2는 "woo95"라는 끝의 한글자만 바뀌고 그것이 숫자를 1 증가시키는 매우 유사한 두개의 input을 SHA-256 hash function으로 hash 해본 결과는 아래와 같다.
hash1 digest: b1af56bfd9742434cb8b29279b74fd1c8e9cf979b59a721bbf9bb1ff264ab314
hash2 digest: 6b99d38bfd8cc0235e63502a70aebcd398ffee66098a655ae401a338192b1717
비슷한 input 값에서 나왔다고 볼 수 없을정도로 다른 값이 나왔다 👾
Hash function이 비밀번호 저장에 적합한 이유는 그들이 deterministic 하기 때문이다.
Deterministic function은 같은 input이 언제나 같은 output을 생성하는 함수이다. 이것은 authentication에서 매우 중요한데 그 이유는 건내받은 비밀번호를 hash한 결과가 저장된 hash 값과 동일해야하기 때문이다. 그렇지 않으면 이 방식으로 제대로 된 비밀번호가 넘어왔는지 확인할 수 없기 때문이다.
여태까지의 설명을 보면 hashing은 생각보다 강력해보인다. 하지만 만약 공격자가 서버로 들어와서 hash된 값들을 탈취한다면, 이야기가 달라진다. 이 hash 값을 만들어내는 input 값을 offline에서 계속해서 찾는다면 서버에 알리지 않고도 사용자의 비밀번호를 알아낼 수 있다.
이런 hash function에 대한 대표적인 공격으로 brute force attack이 있다. Hash value와 일치하는 결과를 내는 input 값을 얻기 위해 무작위의 문자열을 넣어서 시도하는 것이다. Hash function을 시간이 오래걸리도록 만들어서 이런 brute force를 저지할수는 있다(완벽히 막는것은 아니고, 연산의 시간을 오래 걸리게 만드는 것이다).
하지만 brute force에서 한발 더 나아간 rainbow table attack의 경우에는 어떤 문자열을 넣었을 때 어떤 hash 값이 나온다는 것을 미리 연산을 마친 다음 database에 저장해 놓는다. Database에 hash value를 query 하면 input 값이 나오게 된다.
Brute force attack과 rainbow table attack의 사례에서 보면, hash value를 만드는데 들어가는 input이 오로지 사용자에게 건내받은 password라는 공통점이 있다. 그렇다면 사용자에게 건내받은 password와 다른 어떤 값을 hash function의 input으로 넣는다면 이것을 완화시킬 수 있지 않을까?
여기에서 출발한 개념이 salt이다.