암호화 해시 함수: 완전한 가이드
암호화 해시 함수는 현대 보안의 숨은 일꾼입니다. 웹사이트에 로그인하거나, Git에 코드를 푸시하거나, 파일을 다운로드하거나, 비트코인 거래를 할 때마다 해시 함수가 뒤에서 작동하고 있습니다. 하지만 대부분의 개발자는 매일 사용하면서도 그 작동 원리를 이해하지 못하고, 잘못 사용했을 때 어떤 일이 일어나는지도 모릅니다.
이 가이드는 수학적 원리, 역사, 깨진 알고리즘, 현대 표준, 그리고 해시 함수를 올바르게 사용하기 위한 실용적인 코드를 모두 다룹니다.
1. 암호화 해시 함수란?
암호화 해시 함수는 임의 크기의 입력을 받아 고정 크기의 출력인 다이제스트 또는 해시값을 생성합니다. 예를 들어, SHA-256은 입력이 단일 문자이든 전체 영화 파일이든 항상 정확히 256비트(64개의 16진수 문자)를 출력합니다.
핵심 특성
결정론적: 동일한 입력은 항상 동일한 출력을 생성합니다. SHA-256("hello")는 항상 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824를 반환합니다.
빠른 계산: 해시 계산은 밀리초 단위로 완료되어야 합니다. 이 효율성은 파일 무결성 검사와 디지털 서명에 중요하지만, 비밀번호 해싱에서는 약점이 됩니다(나중에 설명).
단방향성(역상 저항성): 해시 출력 H가 주어졌을 때, hash(m) = H가 되는 입력 m을 계산으로 찾는 것은 불가능합니다. 해시값만으로는 원본 데이터를 역산할 수 없습니다.
두 번째 역상 저항성: 입력 m1이 주어졌을 때, hash(m1) = hash(m2)가 되는 다른 입력 m2를 계산으로 찾는 것은 불가능합니다. 공격자가 원본 데이터를 알더라도 동일한 해시값을 생성하는 다른 입력을 찾을 수 없습니다.
충돌 저항성: hash(m1) = hash(m2)가 되는 임의의 두 다른 입력 m1과 m2를 계산으로 찾는 것은 불가능합니다. 이는 두 번째 역상 저항성보다 강한 요구사항입니다.
눈사태 효과: 입력의 작은 변화—단 하나의 비트 반전조차—출력을 완전히 바꿉니다. "hello"를 "hellp"로 바꾸면 원래 해시와 전혀 관련 없는 완전히 다른 해시값이 생성됩니다.
SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
SHA-256("hellp") = 9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca7
이 두 해시값 사이에는 예측 가능한 관계가 없습니다—이것이 눈사태 효과입니다.
2. 해시 알고리즘의 역사
MD5 (1991년)
Ronald Rivest가 MD4의 개선판으로 MD5를 설계했습니다. 128비트 다이제스트를 생성하며, 1990년대 전반에 걸쳐 체크섬과 비밀번호 저장에 광범위하게 채택되었습니다. 10년 이상 동안 MD5는 많은 보안 애플리케이션의 기본 선택이었습니다.
SHA-1 (1995년)
미국 국가안보국(NSA)이 디지털 서명 표준의 일부로 SHA-1(Secure Hash Algorithm 1)을 설계했습니다. 160비트 다이제스트를 생성합니다. SHA-1은 TLS/SSL 인증서, 코드 서명, Git 객체 저장소의 주요 해시 알고리즘이 되었습니다.
SHA-2 패밀리 (2001년)
역시 NSA가 설계한 SHA-2는 실제로 6개 함수의 패밀리입니다: SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256. 가장 널리 사용되는 것은 SHA-256과 SHA-512로, 각각 256비트와 512비트 다이제스트를 생성하며 오늘날에도 안전합니다.
SHA-3 / Keccak (2015년)
SHA-1의 약점이 명백해진 후, NIST는 NSA의 SHA-2 설계와 완전히 독립적인 새로운 해시 표준을 찾기 위한 공개 경쟁(2007~2015년)을 개최했습니다. 우승자는 Guido Bertoni, Joan Daemen, Michaël Peeters, Gilles Van Assche가 설계한 Keccak이었습니다. Merkle–Damgård 구조를 사용하는 SHA-2와 달리, SHA-3은 스펀지 구조를 사용하여 근본적으로 다른 보안 프로파일을 제공합니다.
BLAKE2 (2012년)
BLAKE2는 MD5보다 빠르면서 SHA-3 수준의 보안을 제공하는 암호화 해시 함수입니다. SHA-3 경쟁의 결선 진출자였던 BLAKE의 개선판으로, Jean-Philippe Aumasson, Samuel Neves, Zooko Wilcox-O'Hearn, Christian Winnerlein이 설계했습니다. BLAKE2b는 64비트 플랫폼에, BLAKE2s는 32비트 플랫폼에 최적화되어 있습니다.
3. SHA-256의 작동 원리
SHA-256은 Merkle–Damgård 구조를 사용합니다: 메시지는 고정 크기 블록으로 나뉘고, 압축 함수가 반복적으로 적용되어 한 블록의 출력이 다음 블록의 입력이 됩니다.
1단계: 패딩
입력 메시지는 총 길이가 512비트의 배수가 되도록 패딩됩니다. 1 비트 하나가 추가되고, 이어서 여러 개의 0이 추가되며, 마지막으로 원본 메시지 길이가 64비트 빅엔디언 정수로 추가됩니다.
2단계: 메시지 스케줄
각 512비트 블록은 비트를 혼합하고 회전하는 스케줄을 사용하여 64개의 32비트 워드로 확장됩니다. W[0]에서 W[15]는 메시지 블록에서 직접 옵니다. W[16]에서 W[63]은 다음과 같이 계산됩니다:
W[i] = σ1(W[i-2]) + W[i-7] + σ0(W[i-15]) + W[i-16]
σ0과 σ1은 특정 비트 회전 및 시프트 연산입니다.
3단계: 압축 — 64라운드
SHA-256은 8개의 작업 변수(a부터 h까지)를 유지하며, 처음 8개 소수의 제곱근의 소수 부분으로 초기화됩니다. 64라운드 각각에서:
T1 = h + Σ1(e) + Ch(e,f,g) + K[i] + W[i]
T2 = Σ0(a) + Maj(a,b,c)
h = g; g = f; f = e; e = d + T1
d = c; c = b; b = a; a = T1 + T2
라운드 상수 K[i]는 처음 64개 소수의 세제곱근의 소수 부분입니다. 이 설계 선택은 상수를 공개적으로 검증 가능하게 만들어 숨겨진 백도어에 대한 우려를 불식시킵니다.
4단계: 출력
모든 블록을 처리한 후, 8개의 작업 변수가 초기 해시값에 추가되어 최종 256비트 다이제스트가 생성됩니다. 이 "피드포워드"는 각 블록의 출력이 이전 모든 블록에 의존하도록 보장합니다.
4. MD5와 SHA-1이 깨진 이유
MD5 충돌 (2004년)
2004년, 왕샤오윈 등이 MD5에 대한 실제 충돌 공격—동일한 MD5 해시를 생성하는 두 가지 다른 입력을 찾는 것—을 실증했습니다. 2008년까지 연구자들은 MD5 충돌을 이용해 실제 CA에서 가짜 SSL 인증서를 위조하여 HTTPS 인프라에 대한 실제 공격을 시연했습니다.
이 공격은 정교한 차분 암호 분석을 사용하며 현대 하드웨어에서 몇 초 만에 MD5 충돌을 생성할 수 있습니다.
SHA-1 "SHAttered" 공격 (2017년)
Google의 Project Zero 팀과 CWI Amsterdam은 2017년 SHAttered라고 불리는 최초의 실제 SHA-1 충돌을 생성했습니다. 그들은 동일한 SHA-1 해시를 가진 두 개의 다른 PDF 파일을 생성했습니다. 이 공격에는 약 9.2 × 10¹⁸번의 SHA-1 계산이 필요했습니다—단일 CPU로 6,500년 상당이지만 GPU로는 약 110년으로 국가 수준 조직에게는 충분히 실행 가능합니다.
실제 영향
MD5와 SHA-1은 다음 용도에는 안전하지 않습니다:
- 디지털 서명
- 인증서 지문
- 비밀번호 저장
- 보안에 민감한 모든 애플리케이션
다음 용도에는 여전히 허용됩니다:
- 비암호화 체크섬(신뢰할 수 있는 채널에서 파일 다운로드 무결성 확인)
- 해시 테이블 조회
- 비보안 중복 제거
- 레거시 시스템 호환성(적절한 주의사항 포함)
5. 실제 사용 사례
비밀번호 저장
비밀번호를 일반 텍스트로 저장하는 것은 절대 하지 마세요—심지어 단순 해시 형태로도 안 됩니다. 데이터베이스가 유출되면 공격자는 사전 공격이나 레인보우 테이블을 사용하여 몇 시간에서 며칠 안에 일반 해시를 해독할 수 있습니다.
올바른 접근 방식은 비밀번호를 위해 특별히 설계된 느리고 솔트된 해시 함수를 사용하는 것입니다: bcrypt, scrypt, 또는 Argon2.
파일 무결성 확인
소프트웨어를 다운로드할 때 개발자는 SHA-256 체크섬을 제공합니다. 다운로드 후 파일의 해시를 계산하여 비교합니다. 일치하면 파일이 전송 중에 손상되거나 변조되지 않았음을 의미합니다.
sha256sum downloaded-file.tar.gz
# 개발자가 공개한 체크섬과 비교
디지털 서명
해시 함수는 디지털 서명의 기반입니다. 전체 문서(수 GB가 될 수 있음)에 서명하는 대신, 해시에만 서명합니다. 수신자는 독립적으로 문서를 해시하고 그 해시에 대해 서명을 검증합니다.
블록체인
비트코인은 작업 증명 채굴과 트랜잭션 블록 해싱에 SHA-256을 두 번 적용(SHA-256d)합니다. 채굴자들은 해시했을 때 특정 수의 앞자리 0이 나오는 출력을 생성하는 입력(논스)을 찾아야 합니다—이 과정에는 엄청난 계산 작업이 필요하며 블록체인의 보안을 보장합니다.
Git 객체 저장
Git은 모든 커밋, 트리, 블롭 객체의 해시에 SHA-1을 사용합니다. 해시는 객체의 식별자와 무결성 검사 역할을 모두 합니다. SHA-1의 약점 때문에 Git은 SHA-256으로 적극적으로 마이그레이션하고 있습니다.
스토리지 중복 제거
백업 시스템과 콘텐츠 주소 지정 스토리지(IPFS 등)는 중복 콘텐츠를 식별하기 위해 해시를 사용합니다. 두 파일의 해시가 동일하면 한 번만 저장됩니다.
6. 실제 해시 계산
JavaScript (Node.js)
const crypto = require('crypto');
// SHA-256
const sha256 = crypto.createHash('sha256')
.update('Hello, World!')
.digest('hex');
console.log(sha256);
// 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3
// MD5 (비보안 용도에만 사용)
const md5 = crypto.createHash('md5')
.update('Hello, World!')
.digest('hex');
console.log(md5);
// 65a8e27d8879283831b664bd8b7f0ad4
// SHA-512
const sha512 = crypto.createHash('sha512')
.update('Hello, World!')
.digest('hex');
console.log(sha512);
Python
import hashlib
# SHA-256
h = hashlib.sha256(b"Hello, World!").hexdigest()
print(h)
# 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3
# SHA-512
h512 = hashlib.sha512(b"Hello, World!").hexdigest()
print(h512)
# 여러 알고리즘
for algo in ['md5', 'sha1', 'sha256', 'sha512']:
h = hashlib.new(algo, b"Hello, World!").hexdigest()
print(f"{algo}: {h}")
Bash / 셸
# SHA-256
echo -n "Hello, World!" | sha256sum
# 315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3 -
# MD5
echo -n "Hello, World!" | md5sum
# 65a8e27d8879283831b664bd8b7f0ad4 -
# SHA-1
echo -n "Hello, World!" | sha1sum
# 파일 해시
sha256sum /path/to/file.iso
7. HMAC: 해시 기반 메시지 인증 코드
단순 해시는 데이터 무결성을 검증합니다—데이터가 손상되었는지 알려줍니다. 하지만 진위성은 검증하지 않습니다—누가 데이터를 만들었는지는 증명할 수 없습니다. 누구나 해시를 계산할 수 있기 때문입니다.
HMAC(RFC 2104)는 비밀 키를 해시 함수와 결합하여 이 문제를 해결합니다:
HMAC(K, m) = hash((K' ⊕ opad) || hash((K' ⊕ ipad) || m))
여기서 K'는 블록 크기로 패딩된 키이고, opad/ipad는 특정 패딩 상수입니다. 이 구조는 기반 해시 함수가 안전하다면 증명 가능하게 안전합니다.
일반적인 용도
API 인증: REST API는 HMAC-SHA256을 사용하여 요청에 서명합니다. 서버와 클라이언트는 비밀 키를 공유합니다. 클라이언트는 키로 요청 본문에 서명하고, 서버는 서명을 검증합니다.
JWT 서명: JSON Web Token은 HMAC-SHA256(HS256)을 사용하여 헤더와 페이로드에 서명하여 토큰이 변조되지 않았음을 보장합니다.
웹훅 검증: GitHub, Stripe 등 많은 서비스가 HMAC-SHA256으로 웹훅 페이로드에 서명하여 수신자가 페이로드의 진위를 검증할 수 있습니다.
HMAC 계산
// Node.js
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'my-secret-key')
.update('message to authenticate')
.digest('hex');
console.log(hmac);
import hmac
import hashlib
key = b'my-secret-key'
message = b'message to authenticate'
sig = hmac.new(key, message, hashlib.sha256).hexdigest()
print(sig)
8. 레인보우 테이블과 솔팅
레인보우 테이블이란?
레인보우 테이블은 알려진 해시값을 원래 평문 입력에 매핑하는 사전 계산된 데이터베이스입니다. 공격자가 비밀번호 해시 데이터베이스를 획득하면 각 해시를 개별적으로 크랙할 필요 없이 테이블에서 조회하면 됩니다.
MD5와 SHA-1의 경우, 길이 8자 이하의 모든 ASCII 비밀번호를 포함하는 레인보우 테이블이 수년 동안 무료로 제공되어 왔습니다. CrackStation 같은 웹사이트는 수십억 개의 해시-비밀번호 매핑 데이터베이스를 유지하고 있습니다.
솔팅이 레인보우 테이블을 무력화하는 방법
솔트는 해싱 전에 비밀번호에 추가되는 무작위 값입니다:
hash(salt + password) = stored_hash
솔트는 해시와 함께 저장됩니다(비밀로 할 필요 없음). 각 사용자에게 고유한 무작위 솔트가 부여되므로 공격자는 사전 계산된 테이블을 사용할 수 없습니다—가능한 모든 솔트 값에 대해 별도의 레인보우 테이블이 필요하며 이는 계산상 불가능합니다.
bcrypt: 자동 솔팅과 의도적인 느림
bcrypt는 1999년 비밀번호 해싱을 위해 특별히 설계되었습니다. 자동으로 무작위 솔트를 생성하고 포함시키며, 해시 계산 속도를 제어하는 비용 인수를 포함합니다:
const bcrypt = require('bcrypt');
// 비밀번호 해시 (비용 인수 12 — 현대 하드웨어에서 약 250ms)
const hash = await bcrypt.hash('사용자비밀번호', 12);
// 검증
const isMatch = await bcrypt.compare('사용자비밀번호', hash);
저장된 해시 형식: $2b$12$EixZaYVK1fsbw1ZfbX3OXePaWxn96p36WQoeG6Lruj3vjPGga31lW
$2b$12$ 접두사에 알고리즘 버전과 비용 인수가 인코딩되어 있습니다—bcrypt가 모든 것을 자동으로 처리합니다.
9. 알고리즘 비교표
| 알고리즘 | 출력 크기 | 속도 | 보안 상태 | 최적 용도 |
|---|---|---|---|---|
| MD5 | 128비트 | 매우 빠름 | ❌ 깨짐 (충돌) | 비보안 체크섬만 |
| SHA-1 | 160비트 | 빠름 | ❌ 깨짐 (SHAttered) | 레거시 시스템만 |
| SHA-256 | 256비트 | 빠름 | ✅ 안전 | 범용, TLS, 서명 |
| SHA-512 | 512비트 | 64비트에서 빠름 | ✅ 안전 | 고보안 애플리케이션 |
| SHA-3/Keccak | 가변 | 보통 | ✅ 안전 | SHA-2 대안 |
| BLAKE2b | 가변 | 매우 빠름 | ✅ 안전 | 성능 중요 해싱 |
| bcrypt | 184비트 | 느림 (의도적) | ✅ 안전 | 비밀번호 저장 |
| Argon2id | 가변 | 느림 (의도적) | ✅ 안전 | 비밀번호 저장 (권장) |
10. 비밀번호 해싱 모범 사례
비밀번호는 사용자 계정의 열쇠이므로 특별한 처리가 필요합니다. 비밀번호 데이터베이스 침해는 치명적인 결과를 초래할 수 있습니다. 다음 규칙을 예외 없이 따르세요:
규칙 1: 일반 텍스트 비밀번호를 저장하지 말 것
이것은 당연한 것처럼 보이지만 여전히 발생합니다. 2019년 Facebook이 수억 개의 비밀번호를 내부적으로 일반 텍스트로 저장하고 있었다는 사실이 밝혀졌습니다.
규칙 2: 비밀번호에 빠른 해시를 사용하지 말 것
MD5, SHA-1, SHA-256, SHA-512는 모두 비밀번호 해싱에는 너무 빠릅니다. 현대 GPU는 초당 수십억 번의 SHA-256 해시를 계산할 수 있어 브루트 포스 공격이 몇 시간 안에 가능합니다.
규칙 3: 목적에 맞는 비밀번호 해싱 알고리즘 사용
bcrypt (권장 최소 기준): 비용 인수 12 이상 사용. 널리 지원되고 검증된 방법입니다.
scrypt: 메모리 집약적이어서 GPU와 ASIC 공격에 저항성이 있습니다. 메모리와 CPU 비용 설정 가능.
Argon2id (현재 권장): 2015년 비밀번호 해싱 경쟁의 우승자. Argon2id는 사이드 채널 공격과 시간-메모리 트레이드오프 공격 모두에 저항성이 있어 권장되는 변형입니다. 최소 구성:
- 메모리: 64 MB
- 반복 횟수: 3
- 병렬성: 4
규칙 4: 비밀번호마다 고유한 솔트 사용
bcrypt/scrypt/Argon2(자동 솔팅 포함)를 사용하더라도 그 중요성을 이해하세요: 동일한 비밀번호는 다른 해시를 생성해야 하며, 하나가 침해되어도 다른 것들이 노출되지 않아야 합니다.
규칙 5: 시간이 지남에 따라 비용 인수 조정
하드웨어가 빨라짐에 따라 비용 인수를 높이세요. bcrypt의 경우 약 250~500ms를 목표로 합니다. 다음 로그인 시 비밀번호를 재해싱합니다.
규칙 6: 페퍼 사용 고려
페퍼는 서버 측 비밀(솔트와 달리 데이터베이스에 저장되지 않음)입니다. 해싱 전에 비밀번호에 추가됩니다: hash(pepper + salt + password). 공격자가 데이터베이스를 훔쳐도 페퍼 없이는 비밀번호를 해독할 수 없습니다.
결론
암호화 해시 함수는 인터넷 전반의 보안, 무결성, 신뢰의 기반입니다. 수학적 특성부터 실제 취약점까지 이해하면 진정으로 안전한 시스템을 구축할 수 있습니다.
핵심 요점:
- SHA-256과 SHA-512는 범용 해시의 첫 번째 선택
- MD5와 SHA-1은 암호화 용도로는 깨진 상태
- 비밀번호에는 항상 bcrypt, scrypt, 또는 Argon2 사용
- 무결성뿐만 아니라 인증이 필요할 때는 HMAC 사용
- 솔팅은 레인보우 테이블을 무력화하며, bcrypt와 Argon2는 자동으로 처리
Tool3M 해시 생성기를 사용하여 설치 없이 브라우저에서 직접 SHA-256, SHA-512, MD5 등의 해시를 빠르게 계산하세요.