[Python] hashlib, secrets, hmac: 해시와 암호 보안
python hashlib, secrets, hmac, SHA-256, password hash, crypto safe random
정의
암호학 관련 표준 모듈 3개.
hashlib: 해시 함수 (SHA-256, MD5, BLAKE2 등)secrets: 암호학적으로 안전한 난수 (random은 비안전)hmac: HMAC 메시지 인증
암호화(대칭/비대칭 암호)는 표준 라이브러리에 없음 → cryptography 패키지 사용.
hashlib 기본
import hashlib
data = b"hello world"
print(hashlib.sha256(data).hexdigest())
print(hashlib.sha512(data).hexdigest())
print(hashlib.md5(data).hexdigest()) # 보안 X, 무결성 체크만
print(hashlib.blake2b(data).hexdigest())
해시는 단방향. 같은 입력 → 같은 출력. 작은 입력 변경에도 출력 완전 변경.
update로 큰 데이터 처리
import hashlib
h = hashlib.sha256()
with open("big.iso", "rb") as f:
while chunk := f.read(64 * 1024):
h.update(chunk)
print(h.hexdigest())
스트리밍 처리. 메모리 일정.
3.11+ 편의 함수:
with open("big.iso", "rb") as f:
digest = hashlib.file_digest(f, "sha256")
print(digest.hexdigest())
알고리즘 선택
| 알고리즘 | 권장 |
|---|---|
| SHA-256, SHA-512 | 일반 무결성, 디지털 서명 |
| BLAKE2b, BLAKE3 | 빠르고 안전 (SHA-3 대안) |
| SHA-3 | 차세대 표준 |
| MD5, SHA-1 | 충돌 발견됨, 보안 X (체크섬용만) |
| bcrypt, scrypt, Argon2 | 비밀번호 해싱 전용 |
hashlib.algorithms_guaranteed # OS 무관 항상 지원
hashlib.algorithms_available # 시스템 의존
비밀번호 해싱 - 일반 해시 절대 사용 X
# WRONG: 비밀번호에 일반 해시
hashlib.sha256(password.encode()).hexdigest()
# → GPU로 초당 수십억 시도 가능 (브루트 포스)
비밀번호는 **느린 KDF (Key Derivation Function)**을 써야 한다.
표준 라이브러리: scrypt, pbkdf2
import hashlib
import secrets
salt = secrets.token_bytes(16)
password = b"my-secret-password"
# scrypt (느림, 메모리 hard)
key = hashlib.scrypt(password, salt=salt, n=2**14, r=8, p=1, dklen=32)
# pbkdf2 (느림 = 보안)
key = hashlib.pbkdf2_hmac("sha256", password, salt, iterations=600_000)
저장: salt + iterations + hash. 검증 시 같은 salt/iter로 다시 계산해 비교.
Argon2 (현대 권장)
pip install argon2-cffi
from argon2 import PasswordHasher
ph = PasswordHasher()
hash = ph.hash("password")
print(hash)
# $argon2id$v=19$m=65536,t=3,p=4$...
try:
ph.verify(hash, "password")
print("OK")
except VerifyMismatchError:
print("Wrong password")
Argon2는 비밀번호 해싱 전용 최신 표준 (OWASP 권장).
secrets: 안전한 난수
random 모듈은 비결정 시드에서도 통계용. 보안엔 secrets.
import secrets
# 토큰 생성
print(secrets.token_bytes(16)) # 16바이트
print(secrets.token_hex(16)) # 32자 hex
print(secrets.token_urlsafe(16)) # URL 안전 문자열
# 정수
print(secrets.randbelow(100)) # 0 ~ 99
print(secrets.choice([1, 2, 3, 4])) # 랜덤 선택
# 비밀번호 생성
import string
alphabet = string.ascii_letters + string.digits
password = "".join(secrets.choice(alphabet) for _ in range(20))
세션 토큰, API 키, 비밀번호 재설정 토큰 등에 사용.
hmac: 메시지 인증
해시 + 비밀 키로 메시지가 변조 안 됐고 발신자가 키 소유자임을 검증.
import hmac
import hashlib
key = b"shared-secret"
message = b"transfer 100 to alice"
# 서명 생성
signature = hmac.new(key, message, hashlib.sha256).hexdigest()
print(signature)
# 검증
received_signature = signature
expected = hmac.new(key, message, hashlib.sha256).hexdigest()
if hmac.compare_digest(received_signature, expected):
print("authentic")
else:
print("tampered")
compare_digest
타이밍 공격 방지. 두 문자열 길이 무관 일정 시간 비교. 일반 == 쓰면 안 됨.
# WRONG: 타이밍 공격 가능 (다른 문자 위치 따라 시간 다름)
if signature == expected:
...
# CORRECT
if hmac.compare_digest(signature, expected):
...
자주 보는 패턴
파일 무결성 (체크섬)
import hashlib
def file_sha256(path):
h = hashlib.sha256()
with open(path, "rb") as f:
while chunk := f.read(64 * 1024):
h.update(chunk)
return h.hexdigest()
print(file_sha256("download.iso"))
# 공식 사이트 해시와 비교
CSRF 토큰
import secrets
def generate_csrf_token():
return secrets.token_urlsafe(32)
# 폼에 hidden field로
# 제출 시 세션의 토큰과 hmac.compare_digest로 비교
API 서명 (HMAC)
import hmac
import hashlib
import time
def sign_request(secret, method, path, body, ts):
msg = f"{method}\n{path}\n{ts}\n{body}".encode()
return hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
# 클라이언트
ts = str(int(time.time()))
sig = sign_request(SECRET, "POST", "/api/transfer", "amount=100", ts)
# Header: X-Timestamp: ts, X-Signature: sig
# 서버 검증
expected = sign_request(SECRET, "POST", "/api/transfer", "amount=100", ts)
if not hmac.compare_digest(sig, expected):
raise Unauthorized
AWS Signature V4, GitHub Webhook 등이 이 패턴.
비밀번호 저장 (Argon2)
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher()
# 가입
def register(username, password):
hash = ph.hash(password)
db.save_user(username, hash)
# 로그인
def login(username, password):
user = db.get_user(username)
try:
ph.verify(user.hash, password)
if ph.check_needs_rehash(user.hash): # 옵션 변경 시 재해시
user.hash = ph.hash(password)
db.update(user)
return user
except VerifyMismatchError:
return None
보안 체크리스트
- 비밀번호는 절대 일반 해시 X. Argon2 / scrypt / pbkdf2.
- 토큰·세션은
secrets.random금지. - HMAC 검증은
hmac.compare_digest.==금지. - MD5/SHA-1은 보안용 X. 무결성 체크만.
- 솔트는 사용자별 고유. 전역 솔트 의미 없음.
- 솔트는 secrets.token_bytes로. 시간/이름 기반 X.
- 비밀 키는 환경변수 / KMS / Vault. 코드에 하드코딩 X.
💬 댓글