[Node] 단방향 암호화 (crypto) - 레인보우 테이블 문제

Kim Tae Won·2022년 1월 5일
0
post-thumbnail

암호화가 필요할까?

비밀번호와 같은 중요한 정보를 DB에 그대로 저장하는 것은 범죄와 마찬가지라고 한다. 그만큼 개발자는 책임감을 가지고 비밀번호를 관리해야 한다. 따라서 암호화 라는 방법을 사용하는데, 암호화의 정의는 다음과 같다.

암호화(暗號化) 또는 엔크립션(encryption)은 특별한 지식을 소유한 사람들을 제외하고는 누구든지 읽어볼 수 없도록 알고리즘을 이용하여 정보(평문을 가리킴)를 전달하는 과정

  • 출처 : 위키피디아

암호화는 크게 2가지로 나뉜다.

양방향 암호화에 비대칭형 암호화대칭형 암호화가 있기 때문에 3가지로 나뉜다고도 한다

단방향 암호화양방향 암호화

비밀번호뿐만 아니라 계좌번호나 고객의 중요한 정보들은 모두 암호화를 해야한다. 하지만 종류에 따라 단방향 암호화를 해야할 지, 양방향 암호화를 해야할 지 개발자가 판단해야 한다. 단방향과 양방향 암호화를 나누는 기준은 다시 복호화를 할 수 있느냐 없느냐이다. 단방향은 말 그래도 암호화만이 가능하고 다시 풀 수 없는 구조이다. 양방향은 암호화도 가능하고 key를 이용해 복호화도 가능한 구조이다.

그럼 비밀번호는 무엇으로 암호화 해야할까?
바로 단방향 암호화이다. 잘 생각해보면 다시 복호화할 필요가 없다. 즉, 회원가입 시에 비밀번호를 입력받아 암호화 알고리즘을 돌려 암호화한 후 DB에 저장해 둔다면, 추후에 로그인 시, 다시 입력받은 비밀번호를 동일한 암호화 알고리즘을 돌려 DB의 값과 비교만 하면 된다. 따라서 복호화 과정이 아예 필요가 없다

이번 글에서는 단방향 암호화에 대해 주로 설명할 예정이다.

단방향 암호화

단방향 암호화
단방향 암호화를 하는 가장 간단한 방법은 hash 함수를 쓰는 것이다. 데이터를 hash함수에 넣게 되면 결과로 다이제스트(digest)가 나온다. 하지만 여기에는 큰 문제가 있다.
아래 예시를 통해 조금 더 자세히 알아보자.

crypto = require('crypto')
console.log(crypto.createHash('sha512').update('abcabcabc').digest('base64'));
//J3S0PoVa5dsns5KMdZpXqUSsAYqrf4xiz3iAFz+aUNdicPnw3RyJvJzUazi+ol6SZW+tt4QFYEbI8B00cnmFug==
console.log(crypto.createHash('sha512').update('abcabcabc').digest('base64'));
//J3S0PoVa5dsns5KMdZpXqUSsAYqrf4xiz3iAFz+aUNdicPnw3RyJvJzUazi+ol6SZW+tt4QFYEbI8B00cnmFug==
console.log(crypto.createHash('sha512').update('abcabcabc').digest('hex'));
//2774b43e855ae5db27b3928c759a57a944ac018aab7f8c62cf7880173f9a50d76270f9f0dd1c89bc9cd46b38bea25e92656fadb784056046c8f01d34727985ba

cypto모듈을 불러와 createHash 메소드를 통해 암호화를 한다. 인자로는 암호화 알고리즘이 들어가는데, 예시에서는 sha256이 들어갔다. 그 외에도 sha512 등 종류가 매우 많다고 한다. update에는 암호화할 문자열을 넣어준다. digest에는 encoding 방식을 설정한다. encoding방식에는 여러가지가 있는데, hex에 비해 짧은 base64를 일반적으로 사용한다.

Rainbow Table Problem
예를 보면 같은 문자열로 hash 함수를 돌리면 그 결과인 다이제스트가 항상 동일한 것을 알 수 있다. 그래서 비밀번호 암호화에 사용할 수 있는 것이다. 하지만 이러한 특징이 또 다른 문제가 되기도 한다. 바로 레인보우 테이블(Rainbow Table) 문제이다. 해커들이 이러한 특성을 이용해 모든 암호에 대해 데이터베이스화(테이블화) 해두었다면, 결과를 보고 원래의 암호를 알아 낼 수 있게 된다. 이렇게 데이터베이스화 해둔 테이블을 가리켜 레인보우 테이블이라고 한다.

Brute Force Attack
또 하나의 문제는 무차별 대입(Brute Force Attack)이다. 이는 조합 가능한 모든 경우의 수를 다 대입해보는 것이다. 일반 평문을 hash를 돌려 저장을 하더라도 그 결과가 동일하게 때문에, 자원만 충분하고 공격에 충분한 시간이 주어진다면, 이러한 무차별 대입을 통해 100%의 정확도로 비밀번호를 알아 낼 수 있게 된다.

먼저 Brute Force Attack을 막기 위한 해결책으로는 Hash 함수를 여러 번 돌리는 방법이 있다. 키 스트레칭(Key Stretching)이라고 한다. Brute Force Attack의 경우 시간적으로 어마어마한 시간이 든다. 하지만 특정 문자열 등으로 추론이 가능하다면 시간이 대폭적으로 줄어들게 될 것인데, 이를 막아주는 방법이 바로 hash를 여러 번 하는 것이다. hash를 반복함으로써, 해커가 해킹을 위해 시도하는 시간이 기하급수적으로 늘어나게 된다. 암호화에 걸리는 시간도 늘어나긴 하지만 그만큼 해커가 최종 다이제스트를 얻기까지 시간이 더 소요되는 것이다. 이 문제도 hash를 어느정도 돌리는 지만 알면 충분히 레인보우 테이블이 만들어질 수 있으므로, salt 알고리즘을 더해 사용한다.

salt 알고리즘 이란 음식에 소금을 뿌려 맛을 변화시키듯이, 암호화할 문자열에 salt라는 랜덤 문자열을 붙여 해시함수를 돌리는 것이다. 그러면, 해커는 rainbow table을 기반으로 공격을 하여도 salting 값을 알 수 없기 때문에, 이를 푸는 것은 거의 불가능에 가깝다. 유저1과 유저2가 '123456'으로 비밀번호를 설정했다고 해보자. 여기서 각각 임의의 salt를 더하여 hash를 거치면 그 결과로 나온 다이제스트가 완전히 다르게 된다. 이를 통해 rainbow 테이블을 이용한 공격을 막을 수 있다.

일반적으로 위 두가지 방법을 혼용하여 사용하게 된다. 입력된 평문에 임의의 문자열 salt를 더한 값을 hash 함수를 수 만번 돌려 다이제스트를 얻어 DB에 저장하는 것이다.

Salting + Key Stretching

즉, 우리는 랜덤 문자열 salt와 암호화한 비밀번호를 DB에 저장해놓으면 된다.
로그인 요청이 오면 해당 들어온 비밀번호에 salt를 더하여 hash 함수를 같은 횟수만큼 돌리게 되어 나온 값과 DB의 값을 비교하여 검증하게 된다.

한 줄로 끝났던 일반 hash함수와는 다르게 구현이 조금 복잡하다.
pbkdf2라는 메소드를 사용하여 구현하게 된다.
아래 예시를 보면서 살펴보자.

crypto.randomBytes(64, (err, buf) => {
  crypto.pbkdf2('비밀번호', buf.toString('base64'), 100000, 64, 'sha512', (err, key) => {
    console.log(key.toString('base64')); 
    //'dWhPkH6c4X1Y71A/DrAHhML3DyKQdEkUOIaSmYCI7xZkD5bLZhPF0dOSs2YZA/Y4B8XNfWd3DHIqR5234RtHzw=='
    //DB저장 코드 넣기
  });
});

pbkdf2()에는 인자가 5가지가 들어간다. 비밀번호, salt, 반복 횟수, 비밀 번호 길이, hash 알고리즘 순이다. 위 예시를 보면 crypto.randomBytes()를 통해 생성한 64바이트 salt가 buf에 담기게 된다. buf는 버퍼형식이므로 buf.toString('base64')를 통해 문자열로 변경해주어야 한다.
해당 함수 내에서 salt와 key값을 DB에 저장하는 코드를 작성하면 된다.

로그인 시에 같은 salt값으로 연산을 해야 비교가 가능해진다. 따라서 비밀번호 비교는 다음과 같이 하면 된다.

crypto.pbkdf2('입력비밀번호', '기존salt', 100000, 64, 'sha512', (err, key) => {
  if(key.toString('base64') === '기존 비밀번호'){
  	//작업 진행
  }
});

실제로 해당 코드들을 기존 시스템에 적용을 하니 잘 작동하였다. 여기서는 콜백형태로 작성하였지만, 개발자의 마음대로 공식문서를 참조하여 작성하여도 된다.

공식문서 : node.js - crypto

다음에는 양방향 암호화에 대해서도 다룰 예정이다.

조금 더 자세한 내용에 대해 알고 싶다면 아래 참고 링크를 확인해보자.

참고

맨 아래의 글에 그림이 있어 조금 더 이해하기 쉬울 것 같으므로 한 번 들어가보는 것을 추천한다.

profile
꿈이 너무나 큰 평범한 컴공 대딩에서 취업 성공!

0개의 댓글