현재는 데이터베이스에 사용자 정보를 저장시 비밀번호가 그대로 노출되어있는 형태로 저장된다.
bcrypt
를 사용하여 사용자의 비밀번호를 암호화하고 저장하려 한다.
이전까지 node.js로 개발을 할때는 cryto
를 사용했지만 이번엔 다른 방식을 찾아보다가 알게되었다.
평문은 노출되니 사용하면 안되고, 해시를 통해 저장하면 평문이 노출되진 않지만 rainbowtable
공격이 가능하다.
bcypt
는 해시에 공격자가 암호를 유추할 수 없도록, 평문 데이터에 의미 없는 데이터인 salt라는 것을 추가하여 rainbowtable
을 어렵게하고, 또한 서버의 컴퓨팅성능에 따라 해싱을 변수(코드상에서 saltRound)로 조절하여 bruteforce
공격을 어렵게 한다.
$ npm install bcrypt
아직은 암호화를 사용할 부분이 개발된 API는 회원가입 뿐이다. 그래서 회원가입 서비스 로직에 사용자가 사용하려는 비밀번호에 적용하려한다.
기존에 개발해 둔 서비스 로직에 아래 과정을 추가하면 된다.
1. salt 생성
2. plain text 와 salt를 이용해 hashing
3. 결과와 salt를 저장.
salt
값의 저장은 추후 개발될 로그인처리나 인증과정해서 사용하려한다.
bcrypt로 입력한 패스워드와 hash된 패스워드를 검증할 때 salt는 필요없다. salt는 rainbowtable 공격을 막는 역할일 뿐이다.
import bcrypt from 'bcrypt';
const saltRounds = 12;
export class Secure {
static genSalt = async () => {
return bcrypt.genSalt(saltRounds);
};
static hash = async (plainPassword, salt) => {
return bcrypt.hash(plainPassword, salt);
};
}
추후에 API들이 개발되면서 복호화 과정에서도 쓰일 것 같아 따로 클래스로 선언해서 분리해 두었다.
bcrypt
를 import 하고 salt를 생성하는 역할의 genSalt
와 사용자가 입력한 패스워트와 salt를 이용하여 해싱을 하는 hash
를 선언했다.
saltRounds
는 클수록 좋지만 서버 사양이 따라줘야한다. 검색 결과 10~12값을 최소값으로 권장하고 있어서 일단 12로 값을 정해두었다.
import { UserDao } from './../dao/user.dao';
import { handleError } from './../model/Error';
import { Secure } from './../util/secure';
export class UserService {
static createUser = async (req) => {
let { id, password, name } = req;
const salt = await Secure.genSalt();
password = await Secure.hash(password, salt);
const exsited = await UserDao.getUser(id);
if (exsited != null) {
throw new handleError(409, 'User already existed');
}
try {
await UserDao.createUser(id, password, salt, name);
const result = await UserDao.getUser(id);
return result;
} catch (err) {
throw new handleError(500, 'Create user fail');
}
};
}
위에서 생성한 클래스를 import 해주고 회원가입 서비스 로직에서 salt를 생성하고 받아온 평문 비밀번호와 salt를 이용해 해시를 진행 후 그 값을 저장해 주면 된다.
코드 작성 후 logger를 통해 생성된 salt
값과 hash
값을 확인해 보았다.
DB에 저장도 잘 된다!
bcrypt npm 페이지에서의 사용법은 아래와 같다.
// Load hash from your password DB.
bcrypt.compareSync(myPlaintextPassword, hash); // true
bcrypt.compareSync(someOtherPlaintextPassword, hash); // false
동기/비동기 방식에 따라 함수명이 다르다.
import bcrypt from 'bcrypt';
const saltRounds = 12;
export class Secure {
static genSalt = async () => {
return bcrypt.genSalt(saltRounds);
};
static hash = async (plainPassword, salt) => {
return bcrypt.hash(plainPassword, salt);
};
static check = async (plainPassword, hashPassword) => {
return bcrypt.compareSync(plainPassword, hashPassword);
};
}
secure.js
에 check
를 만들어 bcrypt.compareSync
의 결과를 반환하도록 만들고,
import { UserDao } from './../dao/user.dao';
import { handleError } from './../model/Error';
import { Secure } from './../util/secure';
export class UserService {
...(생략)
static authenticate = async (body) => {
const { id, password } = body;
const user = await UserDao.getUserforAuth(id);
const compareResult = await Secure.check(password, user.password);
if (compareResult === false) throw new handleError(401, 'Auth Error');
return compareResult;
};
}
사용자 서비스 로직에서 해당 함수를 호출하고 결과를 반환하도록 했다. 여기는 추후에 JWT+refresh token을 발급하는 과정이 추가될 예정이라 이렇게만 작성했다.
비밀번호가 맞으면 200, 틀리면 401 에러를 반환하는 것을 확인할 수 있었다.