비밀번호 암호화

박정훈·2022년 3월 3일
0

Backend

목록 보기
5/6

nodejs 내장 모듈인 crypto를 사용할 것이다. 이는 단방향 암호화를 지원해주는데, 여기서 hash 알고리즘을 사용한다.
hash를 거쳐 나온 값으로는 사용자의 비밀번호를 알아낼 수 없다. 즉 복호화가 안된다. 따라서 비밀번호의 hash값을 MongoDB에 저장하고, 로그인 시에 전달된 비밀번호를 hash하여 저장된 값과 비교해서 로그인을 처리해 보려고 한다.

일단 보안에 매우 취약하다고 알려진 SHA1을 사용 해 봤다.

const crypto = require("crypto");

const isSameCrypto = (password) => {
  console.log(crypto.createHash("sha1").update(password).digest("base64"));
};

const password = "말자";
const password2 = "말자";
console.log(isSameCrypto(password) === isSameCrypto(password2));
// UplUrp/xk23KOgBfsh7rgpfpSvA=
// UplUrp/xk23KOgBfsh7rgpfpSvA=
// true. 동일한 패스워드는 동일한 값이 나온다.

같은 패스워드는 동일한 값이 나온다. 이러한 특징이 위험한 이유는 해커들이 수많은 값들에 대한 다이제스트를 모아놓은 리스트, 즉 레인보우 테이블의 존재 때문이다. 특정 비밀번호에 대한 정보가 테이블에 있다면 그대로 까발려지는 것이다...
그리고 좀 더 보안이 높다는 sha512를 써보니 더 긴 문자열로 반환 되는걸 확인할 수 있었다.
그럼에도 위 방식들에는 한계치가 분명하며, 그 대체제로 salting과 key stretching을 추천해 주고 있다.

Key stretching

key stretching은 기존 문자열의 다이제스트(암호화된 데이터)를 생성하고 생성된 다이제스트로 다시 제이스트를 생성한다.
그러니까 해시 함수를 여러 번 수행..! 한다.

Salt

여러번 돌리는 것으로도 부족하다. 각 횟수별 다이제스트가 레이보우 테이블에 존재 할 수도 있다. 같은 비밀번호를 사용하는 사용자들이라면 하나 가지고도 다수 사용자의 password를 알아내 버릴 것이다. 이를 방지하고자 도입한 것이 Salt 방식이다.

해싱 하기전에 기존의 문자열에 salt를 붙여서 새로운 문자열을 반환한다. 만약만약만약 어쩌다가 한명의 패스워드가 유출되더라도... 동일한 패스워드에 salt, 추가적인 문자열을 붙이고 해싱을 돌렸을 것이기 때문에 동일한 패스워드를 쓰는 다른 사용자들은 비교적 안전하다.

즉.. 최초에 salt를 치고, 해싱이 일어날 때마다 salt를 칠 것이다.

Salt를 추가한 문자열 만들기

randomBytes
crypto에서 제공하는 randomBytes로 랜덤한 문자열을 얻을 수 있다. 콜백 함수가 제공되면 바이트가 비동기적으로 생성되고, 콜백 함수는 err 및 buffer 두 인수로 호출된다.

 const createSalt = () => {
  crypto.randomBytes(64, (err, buffer) => {
    if (err) {
      throw new Error(err);
    }
    console.log(
      `${buffer.length} bytes of random data: ${buffer.toString("base64")}`
      // vY5MKysBPGOaWeWZrSTwP5dOCEbNT6ic8LKRWSSesSw61eryVxyEMk6S1EP3x5NJ3nDr+pWqk9YdABYLtraFwg==
    );
  });
};

pbkdf2를 활용해 해싱하기

pbkdf2
crypto에서 제공하는 pbkdf2로 비밀번호를 해싱할 것이다.

// salting 해주고
const createSalt = () =>
  new Promise((resolve, reject) => {
    crypto.randomBytes(64, (err, buffer) => {
      if (err) reject(err);
      resolve(buffer.toString("base64"));
    });
  });

// 여기서 10000번 돌려서 해싱된 패스워드를 만들어 낸다.
// 인수가 5개나 들어가는 pbkdf2는 차례대로
// password, salt, iterations, keylen, digest, callback func
const hashedPassword = (password) =>
  new Promise(async (resolve, reject) => {
    const salt = await createSalt();
    console.log(`salted result: ${salt}`);
    crypto.pbkdf2(password, salt, 10000, 64, "sha512", (err, derivedkey) => {
      if (err) reject(err);
      resolve(derivedkey.toString("base64"));
    });
  });


const password = "말자";
const result = await hashedPassword(password);
console.log(`hashedPassword : ${result}`);
//salted result: 47GZay8InFZiHkYkvBbobJ1lKaMezhaQmog/GT8Ftc24y8As9ej0YbIkE6xr5mEnJDvanObd+YAdr6UkKZncYQ==
//hashedPassword : 5TjmK8TF/ITjpPK8ONoTlXk3ij8Ox390nyTPD7+3MrcWEtNDZDC6LBULGPLHg0siAo0Tvs0PX07G2S2y1a1Cig==

// 그리고 새로고침 때마다 이 값들은 계속 변했다.

이제 이 값을 DB에 넣고... 아! 그러고보니 매번 값이 변한다는건 앞서 계획 했던것에 차질이 생겼다. 난 해싱 된 값이 항상 동일할 테니 그걸 가지고 해야지~ 하면서 생각했었는데 공부하고 보니 매번 다른 값이 나오게 바뀐 로직을 갖게 되었다. 바본가?
패스워드와 솔트 값을 모두 유저 DB에 저장해야겠다.

profile
그냥 개인적으로 공부한 글들에 불과

0개의 댓글