dev_eun

[Node.js] 패스워드 안전하게 저장하기, node의 bcrypt 본문

Back-end/Node.js

[Node.js] 패스워드 안전하게 저장하기, node의 bcrypt

_eun 2021. 7. 11. 14:15

Node로 패스워드를 저장하기 전에! 이론을 먼저 살펴보자.

 

 

용어 설명

다이제스트 : 암호화된 메세지(결과물)

avalanche 효과 : 아주 조그만 변화에도 결과물은 완전히 달라지는 효과

레인보우 공격 : 공격자가 전처리된 다이제스트를 굉장히 많이 확보한 뒤, 탈취한 다이제스트와 비교해 원본 메세지를 찾아내는 방법

레인보우 테이블 : 레인보우 공격에 사용되는 전처리된 다이제스트 테이블

salt : 단방향 해시 함수에서 다이제스트를 생성할 때 추가되는 임의의 문자열

 

먼저,

패스워드는 당연히 안전하게 암호화해서 저장해야 한다. 그리고 이론적으로 공격자가 무한한 컴퓨팅 능력을 갖고 있다면, 해독할 수 없는 패스워드는 없다. 하지만 공격하는 사람들은 그만한 자원을 갖고 있지 않기 때문에, 암호화 시간이 길어질 수록 공격이 어려워진다. 이 말은 패스워드를 암호화하는 시간 자체를 늘려 공격자가 짧은 시간 안에 엄청난 양의 패스워드를 대조할 수 없게 해야 한다는 것이다.

 

단방향 해시 함수

해시 함수를 사용하여 패스워트 텍스트를 암호화된 메세지로 변환할 수 있다.

이 암호화된 메세지를 다이제스트 라고 한다.

단방향 해시 함수는 원본 메세지를 알고 있을 경우에 다이제스트를 구하는 건 쉽지만, 그 반대로는 할 수 없는 함수를 말한다.

 

이 방법을 쓸 경우에 보통은 SHA 방식을 많이 사용한다. 

어떤 암호화된 key를 생성할 때 사용되는 걸 자주 본 것 같다.

이 방법은 avalanche 효과가 발생하며 패스워드 추론을 어렵게 만드는 중요한 요소 중 하나이다.

 

하지만 해시 함수 자체가 암호화를 위해 설계된 것은 맞지만, 패스워드 암호화를 위한 것이 아니라 짧은 시간에 데이터를 검색하기 위해 설계된 것이다.

 

이 때문에 발생하는 문제가 2가지가 있다.

1. 암호화는 하는 시간이 짧다.

이처럼 암호화를 굉장히 빠른 속도로 할 수 있기 때문에 공격자는 아주 짧은 시간에도 몇 억개의 패스워드를 대조할 수 있다. 게다가 패스워드가 충분히 길고 복잡하지 않으면, 그 속도는 더 빨라질 것이다. 

2. 동일한 패스워드 == 동일한 다이제스트

다른 사용자가 같은 패스워드를 사용하고 있었다면, 두 사용자의 다이제스트는 같다. 만약 공격자가 이 둘 중 하나의 다이제스트를 탈취했고 레인보우 공격을 통해 원본 메세지를 알아냈다면 다른 사용자의 패스워드도 함께 탈취된 것이다.

 

그래서 이러한 문제를 보완하기 위해 salt 라는 것이 추가되었다.

Salt

salt는 단방향 해시 함수에서 다이제스트를 생성할 때 추가되는 임의의 문자열이다.

패스워드에 salt를 추가한 문자열 전체를 해쉬 함수를 통해 다이제스트를 생성한다.

보통은 사용자 패스워드를 DB에 저장할 때 사용자마다 다른 random salt와 다이제스트를 함께 저장한다고 한다.

출처: 네이버D2

 

하지만 이 정도로도 충분하지 않아서 해쉬 함수로 생성된 다이제스트를 다시 해쉬 함수에 넣는 방법도 있다.

이 방법을 키 스트레칭(key stretching)이라고 한다.

 

키 스트레칭(key stretching)

해쉬 함수를 여러 번 반복할 수록 암호화가 오래 걸리게 되고 그 만큼 보안은 더 강력해질 수 있다. 이 방법으로 공격자의 브루투포스 공격이 빠른 속도로 진행될 수 없도록 할 수 있다.

 

이러한 방법을 사용하는 기법에 PBKDF2이다.

PBKDF2(Password-Based Key Derivation Function)

이 기법에서는 인증된 해쉬 함수(sha 같은)와 함께 salt를 사용하며, 여러 번의 키 스트레칭을 수행한다.

 

이 기법 이외에 보편적으로 사용되는 bcrypt가 있다.

bcrypt

bcrypt에서는 blowfish와 salt, work-factor(키스트레칭의 횟수)를 주어 패스워드를 암호화한다. 이 기법은 애초에 패스워드 암호화를 위해 설계되었다고 한다.


Nodejs에서 Bcrypt를 사용해보자!

npm과 node는 이미 설치가 되었다고 가정한다.

 

프로젝트를 생성하고 bcrypt를 설치하고 모듈을 추가한다.

# terminal
$ npm install bcrypt
// testBcrypt.js
const bcrypt = require('bcrypt');
const saltRounds = 10;
const plainTextPassword1 = 'asdf1234';
const plainTextPassword2 = 'qwer1234';

 

bcrypt를 사용하는 방법은 (1)salt와 hash를 따로 하는 방법과 (2)한 번에 하는 방법이 있다.

(1) salt, hash 따로만들어서 실행해보면

const createSaltAndHash = (password, rounds) =>
  bcrypt
    .genSalt(rounds)
    .then((salt) => {
      console.log(`salt\t${salt}`);
      return bcrypt.hash(password, salt);
    })
    .then((hash) => { // hash function
      console.log(`hash\t${hash}`);
    });

createSaltAndHash(plainTextPassword1, saltRounds);

(1) salt와 hash 분리하여 생성

같은 패스워드라도 salt값을 할 때마다 달라진다.

salt를 살펴보면, 처음 부분에 $2b$10 가 있다. $2b 는 bcrypt의 버전, $10 는 saltRounds이다.

그리고 hash 함수의 두 번째 인자에 salt값이 들어가고 다이제스트가 생성된다.

다이제스트는 salt값과 password의 해시값이 합쳐져 있다.

 

(2) salting과 hashing을 한 번에 해보면,

const createDigest = (password, rounds) =>
  bcrypt.hash(password, rounds, (err, hash) => {
    console.log(`hash\t${hash}`);
  });

createDigest(plainTextPassword1, saltRounds);

(2) hash 함수로 한번에 생성

hash 함수에 두 번째 인자로 rounds를 주게 되면 salt를 자동으로 생성해준다.

 

이렇게 생성된 다이제스트를 DB에 저장하고 사용자가 로그인을 시도할 것이다. 그럼 DB에서 hash값을 가져와 사용자가 입력한 password과 대조하면 된다.

(2)로 생성한 hash로 대조해보자. bcrypt의 compare 함수를 사용한다.

// testBcrypt.js
const bcrypt = require('bcrypt');
const saltRounds = 10;
const plainTextPassword1 = 'asdf1234';
const plainTextPassword2 = 'qwer1234';
const hash = '$2b$10$LLEOlGpWW5sRZdRJlrPpOOsnziBtr5PVEw5aeDxUz6tLwdy1sNfJC';

const comparePassword = (password, hash) =>
  bcrypt.compare(password, hash).then((result) => {
    if (result) console.log(password, 'password is valid');
    else console.log(password, 'password is invalid');
    return result;
  });

comparePassword(plainTextPassword1, hash);
comparePassword(plainTextPassword2, hash);

 

이렇듯 node에서 bcrypt 사용법은 간단하다.

 

추가적으로, bcrypt의 hash와 compare에서 async로 사용하는 것을 권장하고 있다. 

node bcrypt의 async/sync

bcrypt의 hashing의 경우에 CPU의 영향을 많이 받는데, sync로 사용하게 되면 이벤트 루프에서 block 당할 수 있기 때문이다. 그래서 간단하게 스크립트를 이용해서 사용할 때는 괜찮지만, 서버에서 사용할 땐 async 모드를 권장하고 있다.

Async

async는 아래 처럼 사용할 수 있다.

const checkUser = async (password, hash) => {
  const match = await bcrypt.compare(password, hash).then((result) => {
    if (result) console.log(password, 'password is valid');
    else console.log(password, 'password is invalid');
    return result;
  });
  console.log(match);

  //   if(match) login success;
};

checkUser(plainTextPassword1, hash);

async 이용

sync 

sync 함수는 따로 제공해준다.

// salt, hash 따로 생성
const salt = bcrypt.genSaltSync(saltRounds);
const hash = bcrypt.hashSync(plainTextPassword1, salt);

// 함께 생성
const hash = bcrypt.hashSync(plainTextPassword1, saltRounds);

// hash와 password 대조
bcrypt.compareSync(plainTextPassword1, hash); // true
bcrypt.compareSync(plainTextPassword2, hash); // false

 

 

 

 

참고한 글

728x90