함께 성장하는 프로독학러

4. pbkdf2-password, 비밀번호 보안 모듈 본문

Programming/Node.js

4. pbkdf2-password, 비밀번호 보안 모듈

프로독학러 2018. 6. 18. 15:17

안녕하세요, 프로독학러 입니다.


이번 포스팅에서는 비밀번호 보안 모듈 중 하나인 pbkdf2-password 에 대해서 알아보도록 하겠습니다.


intro


여러분이 어플리케이션을 운영한다고 가정해봅시다.


유저들로부터 회원가입을 받게 되면 서버에 유저들의 정보를 저장해 둡니다.

그리고 회원가입을 한 유저가 로그인을 시도하면 여러분의 어플리케이션은 서버에서 유저의 정보를 조회해 로그인을 시킬 것입니다.


그렇다면 어떤 방식으로 로그인 요청이 유효한지 확인할 수 있을까요?


유저는 로그인은 위하여 유저네임(아이디)와 비밀번호를 입력하고 어플리케이션에 로그인을 요청할 것입니다.

그럼 어플리케이션은 유저가 입력한 유저네임으로 서버에서 데이터를 조회합니다.

유저가 입력한 유저네임이 서버에 저장되어 있지 않다면 로그인 요청을 받아들이지 않고, 만약 있다면 조회된 데이터의 비밀번호가 사용자가 입력한 비밀번호와 일치하는지 확인하여 로그인 요청을 받아들일것인지 아닐것인지 결정할 것입니다.


그렇다면 서버에는 아이디와 비밀번호를 저장해야 하는것일까요?


그렇게 할 수도 있겠지만 서버에 비밀번호를 직접 저장하는것은 매우 위험한 일입니다.

만약 여러분의 서버가 해킹당한다면 여러 사용자의 비밀번호가 모두 노출되기 때문에 여러분은 그 책임에서 자유롭지 못할 것입니다.


그렇다면 어떻게 해야할까요?


이를 해결하기 위한 비밀번호 보안 모듈이 여러개 있지만 이번 포스팅에서는 NIST(National Institute of Standards and Technology, 미국표준기술연구소)에 의해서 승인된 알고리즘이며 미국 정부 시스템에서도 사용자 패스워드의 암호화된 다이제스트를 생성할 때 사용되는 PBKDF2 를 Node.js 환경에서 사용하는 방법에 대해 알아보겠습니다.


설치


먼저 pbkdf2-password 를 사용하기 위해 프로젝트 파일 경로에서 아래 명령어를 통해 모듈을 설치하겠습니다.


npm install --save pbkdf2-password


테스트를 위한 파일을 생성하겠습니다.

루트 경로에 test_pbkdf2.js 를 생성해 주세요.


(./test_pbkdf2.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import express from 'express';
import bodyParser from 'body-parser';
 
const app = express();
 
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
 
const port = 3000;
 
const loginView = `
  <form action="/login" method="post">
    <input type="text" name="username" placeholder="username">
    <input type="password" name="password" placeholder="password">
    <button type="submit">submit</button>
  </form>
`
 
app.get('/login', (req, res) => {
  return res.send(loginView);
});
 
const userInform = {
  username : 'pro-self-studier',
  password : '1234'
}
 
app.post('/login', (req, res) => {
  var reqU = req.body.username;
  var reqP = req.body.password;
  if(userInform.username === reqU){
    if(userInform.password === reqP){
      return res.send('login success');
    }
    return res.send('login failed');
  }else{
    return res.send('login failed');
  }
});
 
app.listen(port, () => {
    console.log('Express is listening on port', port);
});
 
cs


위의 코드는 GET: /login 으로 접속하면 로그인 양식이 보여지고, 로그인 양식을 통해 사용자가 username 과 password 를 입력하면 POST: /loogin 으로 입력한 정보가 보내져 23번째 줄의 유저 정보와 비교해 로그인 성공여부를 확인하는 내용입니다.


23번째 줄에 정의된 userInform 을 데이터베이스에 저장된 유저 정보라고 생각한다면 지금 코드의 상태는 유저의 비밀번호를 서버에 그대로 저장한 것에 해당합니다.

이렇게 서버에 비밀번호를 직접 저장하는것은 상당히 위험하죠.


우리는 pbkdf2 를 활용하여 유저의 비밀번호를 직접저장하지 않고 데이터에 유저 정보를 저장하도록 하겠습니다.


사용


먼저 pbkdf2 모듈을 불러와 줍시다.


(./test_pbkdf2.js)

1
2
3
import bkfd2Password from 'pbkdf2-password';
 
const hasher = bkfd2Password();
cs


hasher 는 함수로, 비밀번호를 수학적인 연산을 통해 암호화 하는것입니다.

pbkdf2 의 hasher 함수는 인식가능한 단방향 해쉬함수가 아닌 키 스트레칭과 솔팅을 더한 안전한 해쉬 함수를 제공합니다.


hasher 함수의 사용법은 다음과 같습니다.


1
2
3
hasher(opts, function(err, pass, slat, hash){
 
});
cs


hasher 함수의 첫 번째 인자는 options 입니다. options 의 형태는 객체로, password 필드와 salt 필드를 가질 수 있습니다.

*필드명 password, salt 는 해당 모듈에서 지정한 값으로 변경해서 사용하면 안됩니다.

만약 options 값을 준다면 해당 password 나 salt 를 이용하여 비밀번호를 암호화합니다.


두 번째 인자는 콜백함수로, 콜백함수의 인자로 err, pass, salt, hash 를 가집니다.

options 로 주어진 password 를 암호화 할 때 랜덤한 salt 값을 만들어 사용하고, 인자로 들어온 salt 값으로 사용된 salt 값이 무엇인지 알 수 있습니다.

salt 를 이용하여 암호화된 값을 hash 라고 부르며, 이 hash 값을 이용하여 인증을 진행합니다.


이렇게 들으면 제대로 감이 안 잡히실 수도 있습니다만, 예제를 통해 감을 잡아보도록 하겠습니다.


먼저 userInform 의 비밀번호를 해셔함수를 이용해 암호화 해 보도록 하겠습니다.

(콘솔창을 이용하여 암호화를 진행해보겠습니다.)


(./test_pbkdf2.js)

1
2
3
4
5
6
7
8
app.listen(port, () => {
    console.log('Express is listening on port', port);
    hasher({password:'1234', salt:userInform.salt}, (err, pass, salt, hash) => {
      console.log(pass);
      console.log(salt);
      console.log(hash);
    });
});
cs


포트에 리스닝 됐을 때 hasher 함수를 이용하여 기존 userInform 의 비밀번호인 '1234' 를 암호화 하였습니다.

암호화가 완료됐을 때 pass(암호화에 사용된 비밀번호), salt(암호화에 사용된 salt-랜덤으로 생성), hash(암호화된 비밀번호) 를 차례로 콘솔창에 찍어보았습니다.



비밀번호 1234, 그 이후에 세 줄에 걸쳐 salt 값, 두 줄에 걸쳐 암호화된 비밀번호인 hash 값이 찍힙니다.


우리는 여기서 salt 값과 hash 값을 userInform 에 저장해 주도록 합시다.


(./test_pbkdf2.js)

1
2
3
4
5
const userInform = {
  username : 'pro-self-studier',
  salt : 'lPWN7VXb/oZZgTdddZm5IUi/X3aQPiGd5L+IHZSlEv7hkred09BU+cN9udvGp0XGQgHAuumGPDYFHBkzKotcuI09Xkuoin6mgJjM1Do6VWfDAxSqTbriR+DvWorPdhPjNQtqT2u2E1i530OB0CeqY/RS+xLSlS3AaGys3RHgvBc=',
  hash : 'ElPfgaR+KWSiosqNr4yOufANQyY3vC3ds4zJyGEh0wSUBrmbxL7kwHv0Bave34yMMcTINKaWYfZ2SmxnpL0qPw=='
}
cs


위의 코드처럼 유저의 비밀번호를 직접 저장하는 것이 아니라 암호화할 때 사용하는 salt 와 암호화된 값인 hash 값만 저장하고, 인증을 요청할 때 마다 salt 값을 이용해 hash 값을 생성해 서버에 저장된 hash 값과 유저가 입력한 password 를 기반으로 생성한 hash 값을 비교해 인증을 진행합니다.


이를 위해 POST: /login 부분의 API를 다음과 같이 수정합니다.


(./test_pbkdf2.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.post('/login', (req, res) => {
  var reqU = req.body.username;
  var reqP = req.body.password;
  if(userInform.username === reqU){
    hasher({password:reqP, salt:userInform.salt}, (err, pass, salt, hash) => {
      if(hash === userInform.hash){
        return res.send('login success');
      }
      return res.send('login failed');
    });
  }else{
    return res.send('login failed');
  }
});
cs


위의 코드에서 먼저 입력받은 username 이 서버에 저장되어 있는 username 인지 먼저 확인하고, 맞다면 유저가 입력한 비밀번호를 서버에 저장된 salt 를 이용하여 암호화 하여 hash 값을 만든뒤, 새롭게 생성된 hash 값과 서버에 저장된 hash 값을 비교해 로그인을 시켜줍니다.


제대로 작동하는지 확인해 보겠습니다.



틀린 비밀번호를 입력하면,



위와 같이 로그인에 실패했다는 화면이 뜨고, 

제대로 입력했을 시에는



위와 같이 로그인에 성공했다는 화면이 제대로 뜨는 것을 알 수 있습니다.


전체 코드는 다음과 같습니다.


(./test_pbkdf2.js)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import express from 'express';
import bodyParser from 'body-parser';
import bkfd2Password from 'pbkdf2-password';
 
const hasher = bkfd2Password();
 
const app = express();
 
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
 
const port = 3000;
 
const loginView = `
  <form action="/login" method="post">
    <input type="text" name="username" placeholder="username">
    <input type="password" name="password" placeholder="password">
    <button type="submit">submit</button>
  </form>
`
 
app.get('/login', (req, res) => {
  return res.send(loginView);
});
 
const userInform = {
  username : 'pro-self-studier',
  salt : 'lPWN7VXb/oZZgTdddZm5IUi/X3aQPiGd5L+IHZSlEv7hkred09BU+cN9udvGp0XGQgHAuumGPDYFHBkzKotcuI09Xkuoin6mgJjM1Do6VWfDAxSqTbriR+DvWorPdhPjNQtqT2u2E1i530OB0CeqY/RS+xLSlS3AaGys3RHgvBc=',
  hash : 'DRnqo1wTadtdeCXsXvQ4H2NpP1hjB0NoIvapiEYHZXZ4f1woWtmD1SiiWafzMdMpzXFz4MpOkVuTOG5+9gEOo9aQleBaZjAG1mwCzIhftUWbdv3ySC75pyj7WB4NgKGSRnhCSCXAQQVLbCtUFZyqfhj87EAdd4t3X/8CWwUFVAU='
}
 
app.post('/login', (req, res) => {
  var reqU = req.body.username;
  var reqP = req.body.password;
  if(userInform.username === reqU){
    hasher({password:reqP, salt:userInform.salt}, (err, pass, salt, hash) => {
      if(hash === userInform.hash){
        return res.send('login success');
      }
      return res.send('login failed');
    });
  }else{
    return res.send('login failed');
  }
});
 
app.listen(port, () => {
    console.log('Express is listening on port', port);
});
 
cs


* 포트를 리스닝 할 때 hasher 를 이용해 암호화를 진행시킨 코드는 삭제했습니다.


여기까지...


pbkdf2-password 모듈을 활용해 비밀번호를 안전하게 서버에 저장하는 방법에 대해서 알아보았습니다.

pbkdf2 는 hash 를 생성할 때 마다 다른 salt 를 사용하지만, 암호화를 할 때 사용했던 salt 를 그대로 사용하면 hash 값도 같기 때문에 이를 활용해 비밀번호를 안전하게 저장할 수 있습니다.

위의 예제에서는 콘솔창을 이용해 hash 값과 그에 사용된 salt 값을 생성했지만, 실제로 어플리케이션에 활용할때는 회원가입을 할때 API 에 hasher 를 이용하여 사용자별로 각각 다른 salt를 생성해 주면 됩니다.

따라서 서버에 유저의 정보를 저장할 때, 비밀번호를 직접 저장하는것이 아니라 salt 와 hash 값만을 이용하면 안전하게 인증을 진행시킬수 있습니다.


감사합니다.


**참고 자료 (항상 감사드립니다)

https://d2.naver.com/helloworld/318732

https://www.npmjs.com/package/pbkdf2-password


포스팅이 도움이 되셨다면 다녀가셨다는 표시로 공감 부탁드릴게요! (로그인 하지 않으셔도 공감은 가능합니다 ㅎㅎ)

Comments