Node.js :: Bcrypt로 비밀번호 해싱(Hashing)하기

Hayoung·2021년 5월 17일
5

Node.js

목록 보기
2/2

비밀번호 해싱(Password hashing)?🧐

유저가 클라이언트(회원가입 폼 등)에서 입력한 ID, 비밀번호, 이름 등의 정보는 데이터베이스에 저장된다.

데이터베이스에 저장되는 정보 중 특히 비밀번호와 같은 정보는 더욱 더 민감하게 다뤄져야하는데
서버 관리자와 정보를 관리하는 데이터베이스 관리자를 포함하여
유저 본인이 아닌 다른 그 누구도 알아서는 안되며,
설사 유출됐다고 하더라도 비밀번호를 해독하지 못하는 형태로 변형해줘야한다.

예를 들어 유저가 다음과 같은 정보로 회원 가입을 했다고 치자.

ID: user1
PW: 12345

이때 유저가 비밀번호로 입력한 12345라는 그 자체의 문자열Plain Text라고 한다.
비밀번호의 값을 데이터베이스에 Plain Text인 채로 저장한다면
비밀번호가 서버 관리자와 정보를 관리하는 데이터베이스 관리자에게 유출되는 것은 물론이며,
해킹을 당했을 시 당연하게도 비밀번호가 아주 손쉽게 도용될 것이다.

이러한 사태를 막기 위해 비밀번호 해싱(Password hashing)이라는 것을 진행하는 것이다.

해싱(Hashing)이란?

(그림 클릭시 출처로 이동합니다.)

해싱(Hashing)이라는 말은 무언가를 작은 조각으로 잘게 다지고 썬다는 뜻을 가진 Hash로부터 왔다.
('해쉬'브라운 할 때 그 해쉬)

해싱(Hashing)이란, 12345와 같은 Plain Text특정 알고리즘(Hash Function)을 통해 잘게 다져서
fd12fsf@$%4s와 같이 인간이 해독하지 못하는 문자열(Hashed Text)로 변형해주는 것이다.

해싱(Hashing)의 특징

해싱(Hashing)에 대해 짚고 넘어가야할 해싱(Hashing)의 특징에 대해 알아보자.

1️⃣ 단방향이다. 되돌릴 수 없다.

아래 그림 해석:
해싱이란 이런 것을 말하는겁니다. 원래대로 다시 되돌리는 것이 불가능합니다.
해쉬 브라운을 감자로 다시 되돌릴 순 없잖아요.

해싱(Hashing)의 가장 중요한 특징인 단방향이라는 것을 이해할 때 단번에 도움이 됐던 그림이다.

해싱(Hashing)단방향의 특징을 가지고 있기 때문에
(해싱(Hashing)이란?의 일러스트 참조. 화살표가 한방향 뿐임)
Plain TextHashed Text로 변형시키는 것은 가능하지만
Hashed TextPlain Text되돌리는 것은 절대 불가능하다.

2️⃣ 동일한 입력값에 대해 동일한 출력값을 가진다.

https://www.convertstring.com/ko/Hash/SHA256

위의 사이트는 웹에서 간단하게 Plain TextHashed Text해싱(Hashing)해주는 사이트이다.

입력값(Plain Text)로 1234를 입력하니 03AC674216F3E15C761EE1A5E255F067953623C8B388B4459E13F978D7C846F4라는 값으로 해싱(Hashing)되었다.

그 후 다시 한번 해싱(Hashing)을 돌리기 위해 「해시 생성」 버튼을 여러번 클릭해도,
Hashed Text는 변하지 않는다.
Hashing은 동일한 입력값에 대해 동일한 출력값을 가진다는 것이 바로 이런 뜻이다.

해커들은 이 특징을 악용하여 Hashed Text으로부터 Plain Text를 유추한 자료인
레인보우 테이블(Rainbow table)을 이용하여 공격을 한다.
유저들이 많이 사용하는 비밀번호와 그의 Hashed Text를 테이블로 만들어놓고,
해킹해온 Hashed TextPlain Text를 역으로 계산하여 찾아내는 것이다.

개발자들은 이를 방어하기 위해 Salt란?🧂라는 것을 사용한다.

3️⃣ 입력값의 아주 일부만 변경되어도 전혀 다른 결과값을 출력한다.

https://www.convertstring.com/ko/Hash/SHA256

위 사이트에서 Hello WorldHashing하면
A591A6D40BF420404A011733CFB7B190D62C65BF0BCDA32B57B277D9AD9F146E라는 결과가 출력되는데,

Hello worldHashing하면
64EC88CA00B268E5BA1A35678A1B5316D212F4F366B2477232534A8AECA37F3C라는 결과가 출력된다.

대문자와 소문자와 같이 입력값이 아주 일부분의 사소한 차이, 한끝 차이임에도 불구하고
전혀 다른 새로운 Hashed Text를 출력한다.

이 특징을 활용하여 보안을 더 강화하기 위해 Salt란?🧂라는 것을 사용한다.

Salt란?🧂

해싱(Hashing)의 특징2️⃣3️⃣에서
해싱(Hashing)의 취약점 때문에 Salt를 활용한다고 했는데, Salt란 무엇일까?

Salt아주 작은 임의의 랜덤한 텍스트를 말하는데 음식에 소금을 살짝 뿌려 간을 하듯이,
Salt란 실제 비밀번호(Plain Text)에 Salt라는 랜덤한 값을 추가해서
Hashed Text를 구하는 방법이다.

Plain Text에 랜덤한 텍스트인 Salt를 추가해서 Hashing을 하면
Hashing3️⃣의 특성으로 인해, 전혀 다른 Hashed Text가 탄생되기 때문에
해커가 Hashed Text를 이용하여 Plain Text 역으로 유추하기 어렵게 만든다.

Plain Text + Salt ⏩ Hashing ⏩ Hashed Text

이번에 사용해볼 Bcrypt란, Plain TextHashed Text로 변형해주는 Hash Function의 한 종류이다.

이번 글에서는 Bcrypt라는 Hash Function 라이브러리에 Salt를 활용해서
비밀번호를 해싱(Hashing)하고 데이터베이스에 저장해보는 시간을 가져 보고자 한다.


Bcrypt 사용해보기

Bcrypt는 단방향 Hash Function 라이브러리 중 하나로, 비밀번호를 해싱(Hashing) 해준다.

Bcrypt 설치

npm install bcrypt --save

Bcrypt로 비밀번호 해싱(Hashing)하기

이번 예제에서는 Node.js(Express)에서 MongoDB(데이터베이스)에 Mongoose를 이용하여
회원 정보(email, 비밀번호)를 저장해보고자 한다.

이때 회원 정보(email, 비밀번호)가 MongoDB에 저장되기 앞서서
먼저 비밀번호를 해싱(Hashing)해주는 처리를 해야 한다.

(맨처음에 설명했듯이 비밀번호가 Plain Text인 채로 저장되면 안되니까.)

// controllers/registerController.js

import User from "../models/User.js";

export const postRegister = (req, res) => {
  const user = new User(req.body);
  user.save((err, userInfo) => {
    if (err) return res.json({ success: false, err })
    return res.status(200).json({ success: true })
  })
}

위의 코드는 클라이언트로부터 받은 회원 정보를
MongoDB(데이터베이스)에 저장하는 기능을 담당하는 Register Controller 코드이다.

이 코드의 save라는 MongoDB 메소드가 발동되어
회원 정보가 MongoDB에 저장되기 전에 먼저 비밀번호를 해싱(Hashing)해줘야 한다.**

그래서 Mongoosepre라는 메소드를 활용한다.
Mongoosepre 메소드는 Register Controllersave 메소드가 실행되기 전에 실행된다.
save 되기 전에 비밀번호를 Hashing 하기 위해,
pre 메소드 내부에 Hash Function인 Bcrypt를 작성한다.

// models/User.js

import mongoose from "mongoose";
import bcrypt from "bcrypt"; // Bcrypt를 불러옴
const saltRounds = 10; // salt 돌리는 횟수

// Schema 정의
const userSchema = new mongoose.Schema({
  name: {
    type: String,
    maxlength: 30
  },
  email: {
    type: String,
    trim: true,
    unique: 1
  },
  password: {
    type: String,
    minlength: 5
  },
  ...
})

// Mongoose의 pre메소드는 `Register Controller`의 save메소드가 실행되기 전에 실행된다.
// save되기 전에 Hashing을 하기 위해 pre메소드 내부에 Hash Function 작성
userSchema.pre("save", function (next) {
  const user = this; // userSchema를 가르킴

  if (user.isModified('password')) {
    // password가 변경될 때만 Hashing 실행
    // genSalt: salt 생성
    bcrypt.genSalt(saltRounds, function (err, salt) {
      if (err) return next(err);
      bcrypt.hash(user.password, salt, function (err, hashedPassword) {
        // hash의 첫번째 인자: 비밀번호의 Plain Text
        if (err) return next(err);
        user.password = hashedPassword; // 에러없이 성공하면 비밀번호의 Plain Text를 hashedPassword로 교체해줌
        next(); // Hashing이 끝나면 save로 넘어감
      })
    })
  } else {
    // password가 변경되지 않을 때
    next(); // 바로 save로 넘어감
  }
})

const User = mongoose.model("User", userSchema);
export default User;

Hashing 결과 확인!🔍

1️⃣ 서버 기동

먼저 터미널에서 서버를 실행시켜준다.
(각자의 스크립트에 맞춰서 실행)

npm run dev:server

2️⃣ Request 송신

그다음 응답 결과를 확인하기 위해 Insomnia라는 툴에서 테스트를 진행했다.

비밀번호의 Plain Text는 임의로 1234567 설정해서 Request를 보냈다.
응답 결과는 에러 없이 성공했다는 뜻으로 무사히 success에 true가 들어왔다.

3️⃣ MongoDB에 저장된 값 살펴보기

비밀번호의 Plain Text1234567가 잘 Hashing 되었는지 확인하기 위해
실제로 MongoDB에 저장된 값을 살펴보자.

비밀번호가 $2b$10$hOshyAvL1SEW4hMWFGQyw..z8B9kP8F1EOB5JwRJbFcqyoQIa87Ri라는 값으로
Hashing 된 것을 알 수 있다!


참고 사이트

https://stackoverflow.com/questions/14588032/mongoose-password-hashing


미흡하고 틀린 정보가 있다면 지적 부탁드립니다!🙇‍♀️

profile
Frontend Developer. 블로그 이사했어요 🚚 → https://iamhayoung.dev

0개의 댓글