bcrypt
1. 개요
bcrypt는 비밀번호 저장소의 보안을 강화하기 위해 설계된 적응형 해시 함수(Adaptive Hashing Function)로, Blowfish 암호 알고리즘을 기반으로 하여 무차별 대입 공격(Brute-force attack)과 레인보우 테이블 공격을 효과적으로 방어하는 것을 목적으로 합니다.
일반적인 SHA-256과 같은 범용 해시 함수는 설계 목적이 '빠른 연산'에 있습니다. 이는 데이터 무결성 검증에는 유리하지만, 공격자가 초당 수억 번의 해시를 생성할 수 있게 하여 비밀번호 크래킹을 용이하게 만듭니다. 반면 bcrypt는 의도적으로 연산 속도를 늦추는 메커니즘을 도입하여, 공격자가 비밀번호를 하나하나 대조하는 데 드는 시간적 비용을 기하급수적으로 증가시킵니다.
2. 동작 원리
bcrypt는 블록 암호 알고리즘인 Blowfish를 변형하여 작동합니다. 핵심은 적응형 해싱(Adaptive Hashing) 개념에 있습니다. 이는 하드웨어 성능이 향상됨에 따라 해시 연산 속도가 빨라지더라도, 설정값(Cost Factor)을 높임으로써 동일한 수준의 보안 강도를 유지할 수 있게 하는 방식입니다.
일반 해시 vs bcrypt 처리 과정 비교
| 구분 |
일반 해시 (예: SHA-256) |
bcrypt |
| 설계 목적 |
빠른 데이터 무결성 검증 |
느린 비밀번호 저장 및 검증 |
| 연산 속도 |
매우 빠름 (나노초 단위) |
의도적으로 느림 (밀리초 단위) |
| 솔트 처리 |
개발자가 직접 구현 및 저장 필요 |
알고리즘 내부에 자동 포함 |
| 공격 대응 |
하드웨어 성능 향상 시 취약해짐 |
비용 인자 조절로 대응 가능 |
| 처리 흐름 |
입력값 $\rightarrow$ 해시 함수 $\rightarrow$ 결과값 |
입력값 + 솔트 $\rightarrow$ 반복적 암호화 $\rightarrow$ 결과값 |
3. 핵심 메커니즘
솔트(Salt)
솔트는 해시 함수에 입력하기 전 추가하는 무작위 데이터입니다. 동일한 비밀번호라도 사용자마다 서로 다른 솔트를 적용하면 결과 해시값이 완전히 달라집니다. 이를 통해 미리 계산된 해시 값들의 테이블인 레인보우 테이블(Rainbow Table) 공격을 원천적으로 차단합니다. bcrypt는 솔트를 별도의 컬럼에 저장할 필요 없이, 생성된 해시 문자열 내에 함께 포함시켜 관리합니다.
비용 인자(Cost Factor)
비용 인자(Work Factor)는 해시 함수가 내부적으로 반복 수행되는 횟수를 결정하는 지수 값입니다. 반복 횟수는 $2^{\text{cost}}$ 형태로 계산됩니다. 예를 들어 비용 인자가 10이면 $2^{10} = 1,024$번의 반복 연산이 수행됩니다. 이 수치를 높이면 공격자가 비밀번호 하나를 확인하는 데 걸리는 시간이 늘어나 브루트 포스 공격의 효율성을 극도로 떨어뜨립니다.
4. 해시 결과값의 구조 분석
bcrypt로 생성된 해시 문자열은 특정 규칙에 따라 구분자($)로 나뉘어 저장됩니다.
예시 문자열: $2a$12$R9h/cIPz0gi.URQHNoS6uO8S6S6S6S6S6S6S6S6S6S6S6S6S6S6S6
(실제 예시: $2a$12$K8S6S6S6S6S6S6S6S6S6S6uO8S6S6S6S6S6S6S6S6S6S6S6S6S6S6S6`) $\rightarrow$ **`$2a$12$R9h/cIPz0gi.URQHNoS6uOS6S6S6S6S6S6S6S6S6S6S6S6S6S6S6
| 섹션 |
예시 |
설명 |
| 알고리즘 버전 |
$2a$ |
사용된 bcrypt의 버전 (2a, 2b, 2y 등) |
| 비용 인자 |
$12$ |
$\log_2$ 기반의 비용 인자 (여기서는 $2^{12}$회 반복) |
| 솔트(Salt) |
R9h/cIPz0gi.URQHNoS6uO |
22자의 Base64 인코딩된 무작위 솔트 값 |
| 해시값(Hash) |
S6S6S6S6S6S6S6S6S6S6S6S6S6S6S6 |
31자의 솔트와 비밀번호가 결합되어 계산된 최종 해시 |
5. 사용 방법 및 예제
표준 프로세스
- 회원가입:
비밀번호 $\rightarrow$ bcrypt.hash(비밀번호, cost) $\rightarrow$ DB 저장
- 로그인:
입력 비밀번호 + DB 저장 해시 $\rightarrow$ bcrypt.compare(입력값, 저장값) $\rightarrow$ 일치 여부 반환
언어별 구현 예시
const bcrypt = require('bcryptjs');
async function handlePassword() {
const password = "mySecurePassword123";
const saltRounds = 12;
// 해싱 (async/await 패턴 사용)
const hash = await bcrypt.hash(password, saltRounds);
console.log("Hashed Password:", hash);
// 검증
const isMatch = await bcrypt.compare(password, hash);
console.log("Match:", isMatch); // true or false
}
handlePassword();
import bcrypt
password = b"mySecurePassword123" # bcrypt는 bytes 타입을 요구함
# 해싱
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# 검증
if bcrypt.checkpw(password, hashed):
print("Password matches")
6. 비용 인자 설정 가이드라인
비용 인자는 보안성과 사용자 경험(UX) 사이의 트레이드오프(Trade-off) 관계에 있습니다. 너무 낮으면 보안이 취약하고, 너무 높으면 서버 CPU 부하가 증가하여 로그인 응답 속도가 느려집니다.
- 권장 기준: 단일 로그인 요청 처리 시간이 100ms ~ 500ms 사이가 되도록 설정하는 것이 일반적입니다.
- 설정 방법:
- 벤치마킹: 현재 서버 환경에서
cost 값을 10, 11, 12 등으로 변경하며 실제 해싱에 걸리는 시간을 측정합니다.
- 최적값 선택: 측정 결과가 100ms~500ms 범위에 들어오는 가장 높은
cost 값을 선택합니다.
- 주기적 업데이트: 하드웨어 성능이 향상되어 처리 시간이 100ms 미만으로 떨어지면,
cost 값을 1씩 높여 보안 강도를 유지합니다. (예: 100ms $\rightarrow$ 50ms로 단축 시, cost를 10에서 11로 상향)
- 일반적 기준: 현재 대부분의 웹 서비스에서는 10 ~ 12 사이의 값을 주로 사용합니다.
7. 보안성 및 한계
방어 가능한 공격
- 레인보우 테이블 공격: 개별 솔트 적용으로 방어.
- 브루트 포스/딕셔너리 공격: 비용 인자를 통한 연산 지연으로 방어.
기술적 제약: 72바이트 제한
bcrypt는 입력값의 최대 길이를 72바이트로 제한합니다. 72바이트를 초과하는 입력값은 그 이후의 문자가 무시(Truncation)되어, 비밀번호 뒷부분이 달라도 동일한 해시값이 생성되는 보안 취약점이 발생할 수 있습니다. 따라서 매우 긴 비밀번호를 허용하는 시스템에서는 반드시 아래의 해결 방안을 적용해야 합니다.
해결 방안: 사전 해싱(Pre-hashing)
bcrypt에 넣기 전 SHA-256과 같은 빠른 해시 함수로 먼저 해싱하여 입력 길이를 고정시킨 뒤, 그 결과값을 bcrypt에 전달합니다.
import hashlib
import bcrypt
password = "very_long_password_exceeding_72_bytes_..."
# 1. SHA-256으로 사전 해싱 (결과값은 항상 32바이트의 bytes 타입으로 고정)
pre_hashed = hashlib.sha256(password.encode()).digest()
# 2. 사전 해싱된 값을 bcrypt로 처리
# pre_hashed와 gensalt()의 결과 모두 bytes 타입이어야 TypeError를 방지할 수 있음
final_hash = bcrypt.hashpw(pre_hashed, bcrypt.gensalt())
8. 관련 기술 및 대안
최근에는 GPU나 ASIC(전용 집적 회로)을 이용한 병렬 연산 공격이 강력해짐에 따라, CPU 연산뿐만 아니라 메모리 사용량까지 강제하는 '메모리 하드(Memory-hard)' 함수들이 등장했습니다.
| 특성 |
bcrypt |
scrypt |
Argon2 |
| 주요 제어 요소 |
CPU 시간 (Cost) |
CPU 시간 + 메모리 |
CPU 시간 + 메모리 + 병렬성 |
| GPU 공격 저항성 |
보통 |
높음 |
매우 높음 |
| 메모리 하드 |
아니오 |
예 |
예 |
| 표준 여부 |
사실상 표준 |
IETF 표준 |
PHC 우승 |
| 추천 사용처 |
일반적인 웹 서비스 |
높은 보안 요구 서비스 |
최신 고보안 시스템 (권장) |
선택 기준:
- 기존 시스템과의 호환성이 중요하고 일반적인 수준의 보안이 필요하다면 $\rightarrow$ bcrypt
- GPU 기반의 대규모 병렬 공격까지 철저히 방어해야 한다면 $\rightarrow$ Argon2id
- 참고: Argon2는 구현 방식에 따라 Argon2i(사이드 채널 공격 방어), Argon2d(GPU 공격 방어), Argon2id(두 방식의 하이브리드)로 나뉘며, 일반적인 비밀번호 해싱에는 Argon2id가 가장 권장됩니다.
# bcrypt
## 1. 개요
**bcrypt**는 비밀번호 저장소의 보안을 강화하기 위해 설계된 적응형 해시 함수(Adaptive Hashing Function)로, Blowfish 암호 알고리즘을 기반으로 하여 무차별 대입 공격(Brute-force attack)과 레인보우 테이블 공격을 효과적으로 방어하는 것을 목적으로 합니다.
일반적인 SHA-256과 같은 범용 해시 함수는 설계 목적이 '빠른 연산'에 있습니다. 이는 데이터 무결성 검증에는 유리하지만, 공격자가 초당 수억 번의 해시를 생성할 수 있게 하여 비밀번호 크래킹을 용이하게 만듭니다. 반면 bcrypt는 의도적으로 연산 속도를 늦추는 메커니즘을 도입하여, 공격자가 비밀번호를 하나하나 대조하는 데 드는 시간적 비용을 기하급수적으로 증가시킵니다.
## 2. 동작 원리
bcrypt는 블록 암호 알고리즘인 **Blowfish**를 변형하여 작동합니다. 핵심은 **적응형 해싱(Adaptive Hashing)** 개념에 있습니다. 이는 하드웨어 성능이 향상됨에 따라 해시 연산 속도가 빨라지더라도, 설정값(Cost Factor)을 높임으로써 동일한 수준의 보안 강도를 유지할 수 있게 하는 방식입니다.
### 일반 해시 vs bcrypt 처리 과정 비교
| 구분 | 일반 해시 (예: SHA-256) | bcrypt |
| :--- | :--- | :--- |
| **설계 목적** | 빠른 데이터 무결성 검증 | 느린 비밀번호 저장 및 검증 |
| **연산 속도** | 매우 빠름 (나노초 단위) | 의도적으로 느림 (밀리초 단위) |
| **솔트 처리** | 개발자가 직접 구현 및 저장 필요 | 알고리즘 내부에 자동 포함 |
| **공격 대응** | 하드웨어 성능 향상 시 취약해짐 | 비용 인자 조절로 대응 가능 |
| **처리 흐름** | `입력값` $\rightarrow$ `해시 함수` $\rightarrow$ `결과값` | `입력값 + 솔트` $\rightarrow$ `반복적 암호화` $\rightarrow$ `결과값` |
## 3. 핵심 메커니즘
### 솔트(Salt)
솔트는 해시 함수에 입력하기 전 추가하는 무작위 데이터입니다. 동일한 비밀번호라도 사용자마다 서로 다른 솔트를 적용하면 결과 해시값이 완전히 달라집니다. 이를 통해 미리 계산된 해시 값들의 테이블인 **레인보우 테이블(Rainbow Table)** 공격을 원천적으로 차단합니다. bcrypt는 솔트를 별도의 컬럼에 저장할 필요 없이, 생성된 해시 문자열 내에 함께 포함시켜 관리합니다.
### 비용 인자(Cost Factor)
비용 인자(Work Factor)는 해시 함수가 내부적으로 반복 수행되는 횟수를 결정하는 지수 값입니다. 반복 횟수는 $2^{\text{cost}}$ 형태로 계산됩니다. 예를 들어 비용 인자가 10이면 $2^{10} = 1,024$번의 반복 연산이 수행됩니다. 이 수치를 높이면 공격자가 비밀번호 하나를 확인하는 데 걸리는 시간이 늘어나 브루트 포스 공격의 효율성을 극도로 떨어뜨립니다.
## 4. 해시 결과값의 구조 분석
bcrypt로 생성된 해시 문자열은 특정 규칙에 따라 구분자(`$`)로 나뉘어 저장됩니다.
**예시 문자열:** `$2a$12$R9h/cIPz0gi.URQHNoS6uO8S6S6S6S6S6S6S6S6S6S6S6S6S6S6S6`
(실제 예시: `$2a$12$K8S6S6S6S6S6S6S6S6S6S6uO8S6S6S6S6S6S6S6S6S6S6S6S6S6S6S6`) $\rightarrow$ **`$2a$12$R9h/cIPz0gi.URQHNoS6uO`**`S6S6S6S6S6S6S6S6S6S6S6S6S6S6S6`**
| 섹션 | 예시 | 설명 |
| :--- | :--- | :--- |
| **알고리즘 버전** | `$2a$` | 사용된 bcrypt의 버전 (2a, 2b, 2y 등) |
| **비용 인자** | `$12$` | $\log_2$ 기반의 비용 인자 (여기서는 $2^{12}$회 반복) |
| **솔트(Salt)** | `R9h/cIPz0gi.URQHNoS6uO` | **22자**의 Base64 인코딩된 무작위 솔트 값 |
| **해시값(Hash)** | `S6S6S6S6S6S6S6S6S6S6S6S6S6S6S6` | **31자**의 솔트와 비밀번호가 결합되어 계산된 최종 해시 |
## 5. 사용 방법 및 예제
### 표준 프로세스
1. **회원가입:** `비밀번호` $\rightarrow$ `bcrypt.hash(비밀번호, cost)` $\rightarrow$ `DB 저장`
2. **로그인:** `입력 비밀번호` + `DB 저장 해시` $\rightarrow$ `bcrypt.compare(입력값, 저장값)` $\rightarrow$ `일치 여부 반환`
### 언어별 구현 예시
#### Node.js (bcryptjs)
```javascript
const bcrypt = require('bcryptjs');
async function handlePassword() {
const password = "mySecurePassword123";
const saltRounds = 12;
// 해싱 (async/await 패턴 사용)
const hash = await bcrypt.hash(password, saltRounds);
console.log("Hashed Password:", hash);
// 검증
const isMatch = await bcrypt.compare(password, hash);
console.log("Match:", isMatch); // true or false
}
handlePassword();
```
#### Python (bcrypt)
```python
import bcrypt
password = b"mySecurePassword123" # bcrypt는 bytes 타입을 요구함
# 해싱
hashed = bcrypt.hashpw(password, bcrypt.gensalt(rounds=12))
# 검증
if bcrypt.checkpw(password, hashed):
print("Password matches")
```
## 6. 비용 인자 설정 가이드라인
비용 인자는 보안성과 사용자 경험(UX) 사이의 트레이드오프(Trade-off) 관계에 있습니다. 너무 낮으면 보안이 취약하고, 너무 높으면 서버 CPU 부하가 증가하여 로그인 응답 속도가 느려집니다.
- **권장 기준:** 단일 로그인 요청 처리 시간이 **100ms ~ 500ms** 사이가 되도록 설정하는 것이 일반적입니다.
- **설정 방법:**
1. **벤치마킹:** 현재 서버 환경에서 `cost` 값을 10, 11, 12 등으로 변경하며 실제 해싱에 걸리는 시간을 측정합니다.
2. **최적값 선택:** 측정 결과가 100ms~500ms 범위에 들어오는 가장 높은 `cost` 값을 선택합니다.
3. **주기적 업데이트:** 하드웨어 성능이 향상되어 처리 시간이 100ms 미만으로 떨어지면, `cost` 값을 1씩 높여 보안 강도를 유지합니다. (예: 100ms $\rightarrow$ 50ms로 단축 시, `cost`를 10에서 11로 상향)
- **일반적 기준:** 현재 대부분의 웹 서비스에서는 **10 ~ 12** 사이의 값을 주로 사용합니다.
## 7. 보안성 및 한계
### 방어 가능한 공격
- **레인보우 테이블 공격:** 개별 솔트 적용으로 방어.
- **브루트 포스/딕셔너리 공격:** 비용 인자를 통한 연산 지연으로 방어.
### 기술적 제약: 72바이트 제한
bcrypt는 입력값의 최대 길이를 **72바이트**로 제한합니다. 72바이트를 초과하는 입력값은 그 이후의 문자가 무시(Truncation)되어, 비밀번호 뒷부분이 달라도 동일한 해시값이 생성되는 보안 취약점이 발생할 수 있습니다. 따라서 매우 긴 비밀번호를 허용하는 시스템에서는 반드시 아래의 해결 방안을 적용해야 합니다.
#### 해결 방안: 사전 해싱(Pre-hashing)
bcrypt에 넣기 전 SHA-256과 같은 빠른 해시 함수로 먼저 해싱하여 입력 길이를 고정시킨 뒤, 그 결과값을 bcrypt에 전달합니다.
```python
import hashlib
import bcrypt
password = "very_long_password_exceeding_72_bytes_..."
# 1. SHA-256으로 사전 해싱 (결과값은 항상 32바이트의 bytes 타입으로 고정)
pre_hashed = hashlib.sha256(password.encode()).digest()
# 2. 사전 해싱된 값을 bcrypt로 처리
# pre_hashed와 gensalt()의 결과 모두 bytes 타입이어야 TypeError를 방지할 수 있음
final_hash = bcrypt.hashpw(pre_hashed, bcrypt.gensalt())
```
## 8. 관련 기술 및 대안
최근에는 GPU나 ASIC(전용 집적 회로)을 이용한 병렬 연산 공격이 강력해짐에 따라, CPU 연산뿐만 아니라 **메모리 사용량**까지 강제하는 '메모리 하드(Memory-hard)' 함수들이 등장했습니다.
### bcrypt, scrypt, Argon2 비교
| 특성 | bcrypt | scrypt | Argon2 |
| :--- | :--- | :--- | :--- |
| **주요 제어 요소** | CPU 시간 (Cost) | CPU 시간 + 메모리 | CPU 시간 + 메모리 + 병렬성 |
| **GPU 공격 저항성** | 보통 | 높음 | 매우 높음 |
| **메모리 하드** | 아니오 | 예 | 예 |
| **표준 여부** | 사실상 표준 | IETF 표준 | PHC 우승 |
| **추천 사용처** | 일반적인 웹 서비스 | 높은 보안 요구 서비스 | 최신 고보안 시스템 (권장) |
**선택 기준:**
- 기존 시스템과의 호환성이 중요하고 일반적인 수준의 보안이 필요하다면 $\rightarrow$ **bcrypt**
- GPU 기반의 대규모 병렬 공격까지 철저히 방어해야 한다면 $\rightarrow$ **Argon2id**
- *참고: Argon2는 구현 방식에 따라 Argon2i(사이드 채널 공격 방어), Argon2d(GPU 공격 방어), Argon2id(두 방식의 하이브리드)로 나뉘며, 일반적인 비밀번호 해싱에는 **Argon2id**가 가장 권장됩니다.*