[회원가입] 비밀번호 암호화

sookyoung.k·2023년 9월 13일
0

🍏 NodeJS

목록 보기
1/10
post-thumbnail

인턴 근무도 벌써... 한 달이 지났군

회원가입, 로그인 기능을 구현하면서 비밀번호 암호화 단계에서 배운 내용을 벨로그에도 정리해보기로!


🥔 Hash

단방향 암호화 기법

해시함수(해시 알고리즘)을 이용하여 고정된 길이의 암호화 된 문자열로 바꿔버리는 것

단방향 해시 함수의 다이제스트(digest)로 사용자의 패스워드를 저장

원본 메시지를 알면 암호화 된 메시지를 구하긴 쉽지만, 암호화 된 메시지로는 원본 메시지를 구할 수 없다 (단방향성)

다이제스트

→ 수학적인 연산을 통해 원본 메시지를 변환하여 암호화 한 메시지

단방향 해시 함수의 보안 문제

인식 가능성

레인보우 공격 (rainbow attack)

  • 동일한 메시지가 동일한 다이제스트를 갖는다면, 공격자가 전처리 된 다이제스트를 가능한 많이 확보한 다음 탈취한 다이제스트와 비교하여 원본 메시지를 찾아낼 가능성이 있다
  • 이와 같은 다이제스트 목록을 레인보우 테이블(rainbow table)이라고 한다

속도

해시 함수의 빠른 처리 속도는 사용자보다 공격자에게 더 큰 편의성을 제공한다

해시 함수 → 짧은 시간에 데이터를 검색하기 위해 설계된 것

때문에 해시 함수의 빠른 처리로 공격자도 매우 빠른 속도로 임의의 문자열의 다이제스트와 해킹할 대상의 다이제스트를 비교할 수 있다

단방향 해시 함수를 보완하기 위한 방법

솔팅(salting)

* 솔트(salt): 단방향 함수에서 다이제스트를 생성할 때 추가되는 바이트 단위의 임의의 문자열

원본 메시지에 문자열(salt)을 추가하여 다이제스트를 생성하는 것

키 스트레칭(key stretching)

입력한 패스워스트의 다이제스트 생성 → 생성된 다이제스트를 입력값으로 하여 다이제스트 생성 → 이를 반복

→ 입력한 패스워드를 동일한 횟수만큼 해시해야만 입력한 패스워드의 일치 여부를 확인할 수 있다

솔팅과 키 스트레칭으로 구성된 암호화 시스템을 구현하려고 한다면 이미 검증된 암호화 시스템을 사용할 것을 권장한다

많이 사용하는 function(라이브러리): PBKDF2, bcrypt, scrypt 등

해시함수 (hash function)

임의의 길이 데이터를 고정된 길이의 데이터로 매핑하는 함수

  • key: 매핑 전 원래 데이터의 값
  • hash value: 매핑 후 데이터의 값
  • hashing: 매핑하는 과정

해시 알고리즘

  • 종류가 다양하고 알고리즘마다 hash 길이가 다르다
  • 모두에게 공개되어 있다 = 해커에게도 공개
  • 이미 보안이 뚫린 해시 함수도 존재 (사용 x): MD5, SHA-1,HAS-180

SHA-256, SHA-512 등을 사용하기를 권고함

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

단방향은 복호화 할 수 없고, 양방향은 복호화 할 수 있다

대부분의 사이트는 비밀번호를 찾을 때 원래의 비밀번호를 알려주는 것이 아니라 재설정을 한다.

패스워드를 복호화를 하는 순간 외부에 노출이 되기 때문에 단방향 암호화를 사용한다. (= 복호화를 할 이유가 없다!)

단방향 암호화 → Hash 알고리즘 사용


crypto 모듈 사용법

crypto

Node.js에 내장되어 있는 내장 모듈 중 하나

→ 문자열을 암호화, 복호화, 해싱할 수 있도록 도와준다

createHash(), update(), digest()

  • createHash(): 사용할 알고리즘
  • update(): 암호화 할 비밀번호
  • digest(): 인코딩 방식

레인보우 테이블 방지 1. salt 기법

→ salt 값을 DB에 저장해야 한다

// 비밀번호 암호화 예시 
    // 1. generateAuthCode()를 통해서 12자리의 숫자로만 이루어진 salt 값을 생성
    const salt: string = generateAuthCode(100000000000, 999999999999, true);
    // 2. 비밀번호 해싱 
    const hashPassword: string = crypto
        // 알고리즘: SHA-256 방식 사용 
        .createHash('sha256')
        // 암호화 할 비밀번호 값과 salt 값을 같이 해싱
        .update(password + salt)
        // 인코딩 방식: hex 
        .digest('hex');
  
    // 3. DB에 회원 정보 등록 → salt 값도 함께 create 해준다 
    const member: User = await User.create({
        email,
        password: hashPassword,
        salt,
        });

salt 값을 생성할 때 자체적으로 랜덤한 숫자를 만들어서 사용할 수도 있지만, crypto 내장 함수를 활용하는 방법도 있다.

crypto.randomBytes()메소드를 통해서 salt를 반환하는 함수를 작성

const salt = crypto.randomBytes(34).toString('base64'); 

* 솔트의 길이는 32바이트 이상이어야 솔트와 다이제스트를 추측하기 어렵다.

salt 값을 DB에 같이 저장해야 하는 이유 → 로그인 과정에서 필요

// 로그인 예시 
    // 1. 유저의 DB에 저장된 salt 값을 찾는다 
    const salt: User = await User.findOne({
        attributes: ['salt'],
        raw: true,
        where: {
          email,
        },
      });

    // 2. 로그인 시 입력한 패스워드와 DB에서 찾은 salt 값으로 비밀번호를 해싱해준다 
    const hashPassword: string = crypto
        .createHash('sha256')
        .update(password + +salt['salt'])
        .digest('hex');

    // 3. 입력받은 패스워드로 해싱한 값과 DB에 저장된 패스워드 값을 비교해 로그인 성공 여부를 판단한다 
    if (user.password == hashPassword) {
        /* 로그인 성공 */
    } else {
        /* 로그인 실패*/
    }

2. 키 스트레칭 기법

솔트와 패스워드를 해시함수에 넣는 과정을 반복

→ 출력 값을 아주 느리게 산출되도록 한다

암호화 방법에는 pbkdf, scrypt, bcrypt 세 가지가 있음

scrypt 방식이 더 안전하다고 하지만 서버에 엄청난 부하가 가해지는 역효과가 발생할 수 있다

pdkdf()

pdkdf() 함수는 5개의 인자가 필요한데 아래와 같다.

crypto.pbkdf2(password, buffer.toString('인코딩 방식'), 반복횟수, digest길이, '암호화 알고리즘')
// 예시
crypto.pbkdf2(password, salt, 9999, 64, 'sha512', function(err, hashed) {
        if(err) {
          console.log(err)
        } else {
          console.log(hashed.toString('base64'))
        }
      })

참고 사이트

https://velog.io/@kaitlin_k/%EC%95%94%ED%98%B8%ED%99%94-%EB%B0%A9%EC%8B%9D


bcrypt 모듈 사용법

bcrypt.hashpw(password, bcrypt.gensalt())

Blowfish 암호를 기반으로 설계된 암호화 함수로, 가장 강력한 해시 메커니즘 중 하나

Bcrypt가 추천되는 이유

좀 더 느리게 설게뙨 암호화 방식이기 때문에 암호화 해독이 조금 더 어렵다 (SHA family는 연산 속도가 매우 빠르다! 공격자의 속도를 늦출 수록 암호화 해독이 어려워진다)

  • ISO-27001 보안 규정을 준수해야 함: PBKDF2 사용
  • 일반적으로 규정을 준수해야 할 상황이 아니라면: Bcrypt 사용 (구현이 쉽고 비교적 강력하다)
  • 보안 시스템을 구현하는데 돈을 많이 쓸 수 있다: Scrypt 사용

bycrpt

동작 매커니즘: 키 스트레칭

필요한 값: 입력한 암호, salt(bcrypt를 활용한 랜덤 생성)

genSaltSync(rounds, minor), hashSync(data, salt)

2차 비밀번호 설정에는 bycrpt 방식을 사용해 암호화를 했다

// 1. salt를 생성한다 
    const saltRounds = 10; // default가 10이다 
    const salt = bycrpt.genSaltSync(saltRounds);

// 2. 해싱한다 
    const hashedPassword = bcrypt.hashSync(secondaryPassword, salt); 

// 3. 암호화 한 2차 비밀번호를 DB의 user 테이블에 업데이트 해준다 
    userInfo.update({
        secondaryPassword: hashedPassword,
      }, {
        // 로그인 과정에서 식별한 회원 이메일
        where: { email: accessToken['userEmail'] }
      })

compareSync(data, encrypted)

설정한 2차 비밀번호는 마이페이지에서 회원정보를 불러올 때 bycrpt의 compareSync() 함수를 통해 비교한다

// 1. 유저 정보를 식별 
    const userInfo: User = await User.findOne({
        where: { email: accessToken['userEmail'] },
    });

// 2. 패스워드 확인 
    if (bcrypt.compareSync(secondaryPassword, userInfo.secondaryPassword)) {
        /* 성공 */
    } else {
        /* 실패 */
    }

성능 설정

salt를 만들 때 사용하는 rounds의 값은 아래에서 선택 가능하다

디폴트는 10이고, 적절한 rounds 값을 선택해 공격에 대비할 수 있다

    rounds=8 // ~40hashes/sec
    rounds=9 // ~20hashes/sec
    rounds=10 // ~10hashes/sec
    rounds=11 // ~5hashes/sec
    rounds=12 // 2~3hashes/sec
    rounds=13 // ~1 sec/hash
    rounds=14 // ~1.5 sec/hash
    rounds=15 // ~3 sec/hash
    rounds=25 // ~1 hour/hash
    rounds=31 // 2~3 days/hash
profile
영차영차 😎

0개의 댓글