Hashing, Salt, Bcrypt. 3) bcrypt

woo94·2023년 12월 4일
0

security

목록 보기
3/3

Recap

이전까지의 글을 통해서 사용자의 비밀번호를 plaintext로(받은 그대로) 저장하는 것은 선택지가 될 수 없음을 알았다. 그 대신 단방향의 hashing을 진행한 비밀번호를 저장하게 하였다. 하지만 이것은 rainbow table 같은 공격에 취약하기 때문에 충분하지 않다. 비밀번호를 저장하는 더 나은 방법으로 hash function에 비밀번호를 넣기 이전에 unique한 salt를 추가하는 방식에 대한 이야기도 했다. 따라서 이상적인 인증 플랫폼에서는 hashing과 salting이 반드시 조합되어 사용되어야 한다.

bcrypt의 탄생 배경

세상에는 SHA2 family나 SHA3 family 와 같이 선택할 수 있는 다양한 암호학적 함수들이 있다. 하지만 SHA family의 문제점 중 하나는 이들은 빠르게 연산이 되도록 설계 되었다는 것이다. 암호학적 함수가 얼마나 hash를 빠르게 계산하는지는 비밀번호가 얼마나 안전한지와 관련된 중요한 요소이다. 이를테면 빠른 연산은 brute-force attack을 더 빠르게 할 수 있다는 것이다.

현대의 CPU들과 GPU들로 구성된 하드웨어는 1초에 수백만개 혹은 수십억개의 SHA-256 hash를 계산할 수 있다. 빠르게 수행되는 함수보다, 우리는 hashing이 느리게 되는 함수를 원한다. 그리고 우리는 미래에 더 빨라진 하드웨어에 대비하여 이것이 adaptive(적응형; 바뀐 조건에 따라서 변할 수 있는 능력)이었으면 좋겠다. 시간이 흐름에 따라서 계속해서 연산이 더 느리게 수행되게 만들 수 있는 것이다.

이러한 요구조건들에 맞추어져 bcrypt 라는 알고리즘이 탄생하게 되었다.

bcrypt란

bcrypt는 Blowfish cipher를 기반으로 설계되었다. b는 Blowfish에서 왔고, crypt는 UNIX password system에서 사용되는 hashing function의 이름에서 왔다.

crypt는 기술의 발전에 따라서 적응(adapt)에 실패한 대표적인 예시이다. 1976년에는 crypt는 1초에 4개 이하의 비밀번호들을 hash 할 수 있었다. 이에 UNIX Team은 crypt의 강력함에 매우 만족했다. 하지만 20년이 지난 후에 더 빠른 컴퓨터와 소프트웨어가 등장해 해당 함수를 사용하여 1초에 20만개의 hashing을 할 수 있게 되었다. 따라서 공격자들은 매우 효율적으로 dictionary attack을 수행할 수 있었다.

Blowfish cipher는 key가 바뀔때를 제외하면 매우 빠르게 동작하는 block cipher이다. 각각의 새로운 key는 4kb의 텍스트를 암호화 하는것과 같은 pre-processing 과정을 필요로 했다. 이것은 상대적으로 다른 block cipher들보다 매우 느렸다. 이런 느린 key changing이 공격 속도를 늦출수 있어 password hashing 방식으로 적합하다고 판단되었다.

bcrypt는 이런 값비싼 key setup phase를 가변적인 횟수만큼 반복하여 hash 계산 과정에 걸리는 작업량과 시간을 늘릴 수 있었다. bcrypt의 가장 큰 장점은 이 횟수를 반복해서 점점 연산의 속도를 늦출 수 있다는 것이었다. 하드웨어 및 소프트웨어의 성능이 좋아져도 이에 적응할 수 있는 것이다.

bcrypt의 또 다른 장점으로는, 그것이 salt를 기본적으로 요구한다는 것이다. 이 hash function이 어떻게 동작하는지를 더 자세히 알아보자.

bcrypt의 작동원리

bcrypt의 설계자는 Blowfish cipher의 값비싼 key setup 알고리즘으로 새로운 key setup 알고리즘인 "eksblowsifh"(expensive key schedule blowfish) 를 만들었다.

Phase 1:

EksBlowfishSetup 이라는 함수가 요구되는 cost, salt와 password를 사용하여 eksblowfish의 상태를 초기화 시킨다. 그 다음 bcrypt는 primary key로부터 subkey들을 설정하는데 사용되는 key derivation으로 구성된 비싼 key schedule을 수행한다. 여기에서 비밀번호가 primary key로 사용된다. 사용자가 좋지 않거나 짧은 비밀번호를 선택했다면 더 긴 비밀번호로 stretch 한다. 이 행위를 key stretching이라고 한다.

우리가 첫번째 phase에서 하는 행위는 key stretching을 통해서 연산의 속도를 줄여 공격속도를 늦추는 것이다.

Phase 2:

Magic value는 192 bit의 OrpheanBeholderScryDoubt 이다. 이 값은 ECB mode에서 이전에 계산된 값으로 부터 eksblowfish를 사용하여 64번 암호화 된다. 이 phase에서 나온 결과 값은 cost 이며, 128 bit의 salt value가 암호화 루프 이후에 합쳐지게 된다.

이 hash의 결과 값은 $2a$, $2y$ 혹은 $2b$ 로 prefix 된다. 이 prefix들은 bcrypt의 사용을 표시하고 그것의 버전을 표기하기 위해 추가된다.

bcrypt는 secure password function의 핵심 특징을 이룰수 있다:

  • preimage resistant
  • salt space가 충분히 커서 rainbow tables와 같은 precomputation attack을 완화 시킨다.
  • 적응형으로 cost 조절이 가능하다.

bcrypt 적용

사용할 수 있는 2개의 라이브러리가 있다:

  • bcrypt
  • bcryptjs

bcrypt vs bcryptjs

bcrypt는 C++ 로 작성된 bcrypt 라이브러리에 바인딩 된 Node.js 모듈이다. Native C++ 코드를 사용하여 해시 연산을 수행한다. C++ 기반으로 더 빠른 성능을 제공한다. 하지만 C++ 코드를 컴파일 해야하기 때문에 설치 과정이 복잡할 수 있으며 특정 시스템에서 호환 문제가 발생할 수 있다.
bcryptjs는 pure javascript로 작성된 bcrypt의 구현체이다. C++ 바인딩을 필요로 하지 않으며 모든 javascript 환경에서 동작한다. 자신들 오피셜로 bcrypt 보다 30% 정도 느리다고 한다.

호환성이 좋은 점 때문에 bcryptjs를 선택하기로 했다.

const bcrypt = require('bcryptjs')

async function hashing(password) {
    console.time("hashing")
    const salt = await bcrypt.genSalt(10)
    console.timeLog("hashing")
    console.log("salt", salt)
    const hashedPassword = await bcrypt.hash(password, salt)
    console.log("hashedPassword", hashedPassword)
    console.timeEnd("hashing")
    return hashedPassword
}

async function compare(password, hashedPassword) {
    console.time("compare")
    const compareResult = await bcrypt.compare(password, hashedPassword)
    console.log("compareResult", compareResult)
    console.timeEnd("compare")
}

async function main() {
    const password = "pa$$word"
    const hashedPassword =  await hashing(password)
    console.log("----------------------------")
    await compare(password, hashedPassword)
}

main()

hashing function

bcrypt.genSalt(10) 을 통해 salt round 10의 salt를 생성한다. 이는 salt를 생성하고 해시 연산을 수행하는 반복 횟수를 의미한다. 이 파라미터의 값이 크면 더 많은 시간과 리소스가 계산에 필요하다.

compare function

사용자가 회원가입을 할 때 입력한 비밀번호를 plain text로 저장하지 않고, 위의 hashing function에서 해시한 값을 db에 저장하게 된다. 사용자가 로그인을 할 시에 입력한 비밀번호와 db에 저장된 해시 값을 비교하여 비밀번호가 맞는지 틀리는지를 판단해주는 함수가 hash.compare 함수이다.

위의 스크립트를 실행하면 아래와 같은 내용이 터미널에 출력된다:

salthashedPassword를 같이 출력했다. hashedPasswordsalt가 포함되어 있는 것을 확인할 수 있다. 그렇기 때문에 compare 함수를 실행할 때 별도로 salt 값을 제공해주지 않아도 된다.

bcrypt.genSalt 함수의 파라미터 값을 바꿔가면서 측정한 시간을 표로 정리해보았다:
측정에 사용된 컴퓨터의 스펙:

MacBook Air M2(2022)
Memory: 8GB
CPU: 8 core
salt roundsalt 생성시간(ms)hash에 걸린 총 시간(ms)compare에 걸린 시간(ms)
50.49635.2084.343
60.63830.2786.936
70.6437.70212.815
80.63943.34822.199
90.4363.67243.25
100.878121.48494.98
110.857200.538184.152
120.617422.344434.371
130.472819.575828.696
140.94922431713

최초 1회 측정이다 보니 수치상의 역전이 일어나는 경우도 있지만 salt round가 10을 넘어가니 확연히 차이가 난다. 이론상 salt round 마다 소요되는 시간은 지수의 그래프를 나타낸다고 한다.

보안의 정도, 하드웨어의 스펙, 사용자의 UX 등을 고려하여 salt round를 정하면 된다.

참고

https://auth0.com/blog/hashing-in-action-understanding-bcrypt/

profile
SwiftUI, node.js와 지독하게 엮인 사이입니다.

0개의 댓글

관련 채용 정보