Node.js를 통한 로그인 기능 구현 중, '보안'의 측면에서 생각해볼 기회가 생겼다. 현재까지 내가 구현한 로직은 비밀번호를 그대로 노출하고 있었다. 사용자의 비밀번호를 그대로 보관하는 것은 아주 위험하다. 다양한 위험에 노출되기 때문에, 적절한 방법으로 비밀번호를 암호화하여 보관하는 방법이 필요하다.
비밀번호를 암호화하는데 적절한 매커니즘인 bcrypt를 통해서 안전하게 비밀번호를 암호화 해보도록 하자.
✔️ 암호화란, 어떤 평문을 암호문으로 바꾸는 것을 말한다. 반대로 암호문을 평문으로 바꾸는 것을 복호화라고 한다.
암호화는 아주 기본적인 정보 보안 방법으로, 데이터가 유출되는 것 자체를 막지는 못하나 데이터가 어떤 정보를 담고 있는 지 알지 못하게 하는 것에 그 의미가 있다. 따라서 사용자의 비밀번호와 같은 중요한 정보의 경우, 다른 사람이 그 비밀번호를 알지 못하게 하지 위해서 반드시 암호화 과정이 필요한 것이다.
📚 단방향 암호화를 위해 만들어진 해시 함수이다. 즉, 복호화는 불가능하다.
그렇다면 복호화가 불가능한데, 어떻게 기존의 비밀번호와 비교하여 옳은 지 판단할 수 있을까? 비밀번호 자체를 검증할 때는 입력받은 값을 암호화해서 저장된 암호화된 값과 비교해서 검증 할 수 있다.
임의의 길이를 갖는 임의의 데이터에 대해 고정된 길이의 데이터로 매핑하는 함수를 Hash 함수라고 하고, 이 함수의 결과물이 Hash 값이다.
📚 기존에 이미 sha256과 같은 해시 함수들이 사용되고 있었으나, sha256과 같은 경우 암호화를 위해 설계된 것이 아니라 짧은 시간 내에 데이터를 빠르게 검색하기 위한 구조로 설계 되어있다. 따라서 빠른 속도가 장점이지만, 보안에는 취약하다. 또한 미리 해시 값들을 계산해놓은 테이블인 Rainbow table, 즉 Rainbow table attack이라는 해시 함수 반환 값을 역추적하는 해킹 방법으로 인한 보안의 취약점들이 존재한다.
bcrypt는 기존의 단방향 해시 함수들이 가지고 있는 취약점들을 보완하여 탄생했다.
단방향 해쉬 함수의 취약점들을 보안하기 위해 일반적으로 2가지 보완점들이 사용된다.
실제 비밀번호 이외에 추가적으로 랜덤한 데이터 값을 더해 해시 값을 계산하는 방법을 말한다. 음식에 간을 치듯이, 유저가 어떤 비밀번호를 설정했든지 상관없이 거기에 난수까지 추가하여 해시함수에 집어넣는 것이다.
즉 비밀번호의 복잡도를 키워 보안을 높이는 것이다. 이렇게 되면 비밀번호의 길이가 매우 길어지고, 비밀번호가 길수록 크래킹하는 데 필요한 연산시간이 늘어나게 되므로 해킹이 더욱 어려워지는 것이다.
단방향 해시 값을 계산한 뒤, 그 해시 값을 해시하고, 또 해시하는 반복 과정을 말한다. 키 스트레칭을 적용하여 동일 장비에서 1초에 5번 정도만 비교할 수 있도록 한다. 즉, 기존 단방향 해시 알고리즘의 빠른 실행속도가 취약점이 됐던 것을 보안하기 위한 방법이다.
✔️ Salting과 Key Stretching을 구현한 해쉬 함수중 가장 널리 사용되는 것이 bcrypt이다.
이제 정말로 암호화를 진행해보자!
먼저 bcrypt 설치가 필요하다.
아래 명령어를 통해 우선 bcrypt를 설치해주도록 하자.
npm install bcrypt
1. bcrypt 모듈을 불러오자.
const bcrypt = require('bcrypt')
혹은,
import bcrypt from 'bcrypt'
2. 비밀번호를 암호화 시켜보자.
const encryptedPassword = bcrypt.hashSync(password, 10) // sync ver
혹은,
const hashedPassword = async (password) => {
return await bcrypt.hash(password, 10)
} // async ver
이렇게 하면 해시 비밀번호를 생성할 수 있다.
좀 더 엄격한 해싱을 위해 salt를 도입해보도록 하자.
const hashedPassword = async (password) => {
const saltRounds = 10;
const salt = await bcrypt.genSalt(saltRounds);
return await bcrypt.hash(password, salt);
}
3. 이제 해싱된 비밀번호를 확인해보자.
const main = async () => {
const finallyHashing = await hashedPassword('hello_world');
console.log('finallyHashing: ', finallyHashing)
}
main()
👋 이렇게 하면 main 함수가 실행되고, 암호화된 비밀번호가 콘솔로 찍혀 출력되는 것을 확인할 수 있다. 즉, 최종적으로 암호화를 성공한 것이다! 👩🏻💻
이렇게 성공적으로 비밀번호가 암호화되어 database에 저장되고, 유저가 로그인에 성공하게 되면 access token이라고 하는 암호화된 유저 정보를 첨부해서 request를 보내게 된다. 이 토큰을 프론트엔드 서버로 전달하면, 프론트엔드에서는 이를 브라우저에 저장해두고 계속 사용한다. 이후 백엔드는 다시 새로운 요청이 들어올 때, 해당 토큰으로 사용자를 식별하는 데에 사용한다.
토큰에 관련된 내용은 다음에 알아보도록 하자.
👍👍👍