더북(TheBook)

3.5.5.1 단방향 암호화

비밀번호는 보통 단방향 암호화 알고리즘을 사용해서 암호화합니다. 단방향 암호화란 복호화할 수 없는 암호화 방식을 뜻합니다. 복호화는 암호화된 문자열을 원래 문자열로 되돌려놓는 것을 의미합니다. 즉, 단방향 암호화는 한번 암호화하면 원래 문자열을 찾을 수 없습니다. 복호화할 수 없으므로 암호화라고 표현하는 대신 해시 함수라고 부르기도 합니다.

복호화할 수 없는 암호화가 왜 필요한지 의문이 들 수도 있습니다. 하지만 생각해보면 고객의 비밀번호는 복호화할 필요가 없습니다. 먼저 고객의 비밀번호를 암호화해서 데이터베이스에 저장합니다. 그리고 로그인할 때마다 입력받은 비밀번호를 같은 암호화 알고리즘으로 암호화한 후, 데이터베이스의 비밀번호와 비교하면 됩니다. 원래 비밀번호는 어디에도 저장되지 않고 암호화된 문자열로만 비교하는 것입니다.

단방향 암호화 알고리즘은 주로 해시 기법을 사용합니다. 해시 기법이란 어떠한 문자열을 고정된 길이의 다른 문자열로 바꿔버리는 방식입니다. 예를 들면 abcdefgh라는 문자열을 qvew로 바꿔버리고, ijklm이라는 문자열을 zvsf로 바꿔버리는 겁니다. 입력 문자열의 길이는 다르지만, 출력 문자열의 길이는 네 자리로 고정되어 있습니다.

노드에서 해시 함수는 다음과 같이 사용합니다.

hash.js

const crypto = require('crypto');

console.log('base64:', crypto.createHash('sha512').update('비밀번호').digest('base64'));
console.log('hex:', crypto.createHash('sha512').update('비밀번호').digest('hex'));
console.log('base64:', crypto.createHash('sha512').update('다른 비밀번호').digest('base64'));

콘솔

$ node hash
base64: dvfV6nyLRRt3NxKSlTHOkkEGgqW2HRtfu19Ou/psUXvwlebbXCboxIPmDYOFRIpqav2eUTBFuHaZri5x+usy1g==
hex: 76f7d5ea7c8b451b773712929531ce92410682a5b61d1b5fbb5f4ebbfa6c517bf095e6db5c26e8c483e60d8385448a6a6afd9e513045b87699ae2e71faeb32d6
base64: cx49cjC8ctKtMzwJGBY853itZeb6qxzXGvuUJkbWTGn5VXAFbAwXGEOxU2Qksoj+aM2GWPhc1O7mmkyohXMsQw==

비밀번호라는 문자열을 해시를 사용해 바꿔봤습니다.

createHash(알고리즘): 사용할 해시 알고리즘을 넣습니다. md5, sha1, sha256, sha512 등이 가능하지만, md5와 sha1은 이미 취약점이 발견되었습니다. 현재는 sha512 정도로 충분하지만, 나중에 sha512마저도 취약해지면 더 강화된 알고리즘으로 바꿔야 합니다.

update(문자열): 변환할 문자열을 넣습니다.

digest(인코딩): 인코딩할 알고리즘을 넣습니다. base64, hex, latin1이 주로 사용되는데, 그중 base64가 결과 문자열이 가장 짧아서 애용됩니다. 결과물로 변환된 문자열을 반환합니다.

▲ 그림 3-8 해시 함수

가끔 nopqrst라는 문자열이 qvew로 변환되어 abcdefgh를 넣었을 때와 똑같은 출력 문자열로 바뀔 때도 있습니다. 이런 상황을 충돌이 발생했다고 표현합니다. 해킹용 컴퓨터의 역할은 어떠한 문자열이 같은 출력 문자열을 반환하는지 찾아내는 것입니다. 여러 입력 문자열이 같은 출력 문자열로 변환될 수 있으므로 비밀번호를 abcdefgh로 설정했어도 nopqrst로 뚫리는 사태가 발생하게 됩니다.

해킹용 컴퓨터의 성능이 발달함에 따라 기존 해시 알고리즘들이 위협받고 있습니다만, 그에 따라 해시 알고리즘도 더 강력하게 진화하고 있습니다. 언젠가 sha512도 취약점이 발견될 것입니다. 그렇게 된다면 더 강력한 알고리즘인 sha3으로 이전하면 됩니다.

현재는 주로 pbkdf2나 bcrypt, scrypt라는 알고리즘으로 비밀번호를 암호화하고 있습니다. 그중 노드에서 지원하는 pbkdf2를 알아보겠습니다. pbkdf2는 간단히 말하면 기존 문자열에 salt라고 불리는 문자열을 붙인 후 해시 알고리즘을 반복해서 적용하는 겁니다.

pbkdf2.js

const crypto = require('crypto');

crypto.randomBytes(64, (err, buf) => {
  const salt = buf.toString('base>64');
  console.log('salt:', salt);
  crypto.pbkdf2('비밀번호', salt, 100000, 64, 'sha>512', (err, key) => {
    console.log('password:', key.toString('base>64'));
  });
});

먼저 randomBytes() 메서드로 64바이트 길이의 문자열을 만듭니다. 이것이 salt가 됩니다. pbkdf2() 메서드에는 순서대로 비밀번호, salt, 반복 횟수, 출력 바이트, 해시 알고리즘을 인수로 넣습니다. 예시에서는 10만 번 반복해서 적용한다고 했습니다. 즉, sha512로 변환된 결괏값을 다시 sha512로 변환하는 과정을 10만 번 반복하는 겁니다.

▲ 그림 3-9 pbkdf2

너무 많이 반복하는 것은 아닌지 걱정될 수도 있지만, 1초 정도밖에 걸리지 않습니다. 이는 컴퓨터의 성능에 좌우되므로 조금 느리다 싶으면 반복 횟수를 낮추고, 너무 빠르다 싶으면 1초 정도가 될 때까지 반복 횟수를 늘립니다.

싱글 스레드 프로그래밍을 할 때 1초 동안 블로킹이 되는 것은 아닌지 걱정할 수도 있습니다. 다행히 crypto.randomBytescrypto.pbkdf2 메서드는 내부적으로 스레드 풀을 사용해 멀티 스레딩으로 동작합니다. 이러한 메서드가 몇 개 있는데 3.6.4절에서 알아보겠습니다.

콘솔

$ node pbkdf2
salt: OnesIj8wznyKgHva1fmulYAgjf/OGLmJnwfy8pIABchHZF/Wn2AM2Cn/9170Y1AdehmJ0E5CzLZULps+daF6rA==
password: b4/FpSrZulVY28trzNXsl4vVfhOKBPxyVAvwnUCWvF1nnXS1zsU1Paq2p68VwUfhB0LDD44hJOf+tLe3HMLVmQ==

randomBytes이므로 매번 실행할 때마다 결과가 달라집니다. 따라서 salt를 잘 보관하고 있어야 비밀번호도 찾을 수 있습니다.

pbkdf2는 간단하지만 bcrypt나 scrypt보다 취약하므로 나중에 더 나은 보안이 필요하면 bcrypt나 scrypt 방식을 사용하면 됩니다. 이 책에서는 나중에 회원의 비밀번호를 암호화할 때 bcrypt 방식을 사용합니다.

 

신간 소식 구독하기
뉴스레터에 가입하시고 이메일로 신간 소식을 받아 보세요.