비밀번호 관련 조언 중 상당수는 틀렸거나 비현실적입니다. “대문자, 소문자, 숫자, 특수문자를 섞어 쓰세요”는 엄격해 보이지만 P@ssw0rd1! 같은 비밀번호를 만들어냅니다. 컴퓨터는 쉽게 크랙하는데 사람은 기억하기 어려운 비밀번호입니다. 이 글에서는 진짜 중요한 것을 정리합니다.
비밀번호를 실제로 강하게 만드는 것
비밀번호 강도의 본질은 엔트로피입니다 — 공격자가 비밀번호를 찾는 데 필요한 추측 횟수입니다.
Tr0ub4dor&3 같은 12자 비밀번호는 복잡해 보입니다. 하지만 일반적인 패턴(단어 + 숫자 치환 + 기호)을 따르기 때문에, 규칙 기반 변형을 사용하는 사전 공격으로 무작위 8자 비밀번호보다 빠르게 크랙될 수 있습니다.
진정한 무작위성은 복잡해 보이는 패턴보다 항상 강합니다.
비밀번호 엔트로피
엔트로피는 비트로 예측 불가능성을 측정합니다. 비트가 하나 늘어날 때마다 탐색 공간이 두 배가 됩니다.
N개 문자 풀에서 길이 L의 비밀번호를 선택할 때:
엔트로피 = L × log₂(N)
| 문자 풀 | 풀 크기 | 12자 엔트로피 |
|---|---|---|
| 소문자만 | 26 | 56비트 |
| 소문자 + 대문자 | 52 | 68비트 |
| 소+대+숫자 | 62 | 72비트 |
| 소+대+숫자+특수문자(32개) | 94 | 79비트 |
| 패스프레이즈 (7776 단어 목록) | 7776 | ~51비트/단어 |
참고 기준:
- 50비트 미만: 현대 하드웨어로 초~시간 내 크랙 가능
- 50~70비트: 속도 제한이 있는 저위험 계정에는 적절
- 70비트 이상: 대부분의 목적에 강력
- 100비트 이상: 미래 대비
강력한 비밀번호 생성하기
브라우저의 crypto.getRandomValues() API를 사용해 암호학적으로 무작위한 비밀번호를 생성합니다. 길이, 문자셋, 개수 옵션을 제공합니다. 서버로 아무것도 전송되지 않습니다.
비밀번호 생성기의 작동 원리
안전한 비밀번호 생성기는 반드시 **암호학적으로 안전한 난수 생성기(CSPRNG)**를 사용해야 합니다. 표준 RNG와는 다릅니다.
// 잘못된 방법: Math.random()은 암호학적으로 안전하지 않음
// 공격자가 시드를 알면 모든 출력을 예측할 수 있음
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let password = '';
for (let i = 0; i < 16; i++) {
password += chars[Math.floor(Math.random() * chars.length)];
}
// 올바른 방법: crypto.getRandomValues()는 암호학적으로 안전함
function generatePassword(length = 16, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*') {
const array = new Uint32Array(length);
crypto.getRandomValues(array);
return Array.from(array, n => charset[n % charset.length]).join('');
}
Python에서:
import secrets
import string
def generate_password(length=16, use_symbols=True):
alphabet = string.ascii_letters + string.digits
if use_symbols:
alphabet += string.punctuation
return ''.join(secrets.choice(alphabet) for _ in range(length))
# secrets 모듈은 os.urandom()을 사용 — 암호학적으로 안전
password = generate_password(20)
주의: Python의 random.choice()는 암호학적으로 안전하지 않습니다. 보안 관련 무작위성에는 항상 secrets.choice()를 사용하세요.
패스프레이즈: 무작위 문자열의 대안
패스프레이즈는 무작위 사전 단어들의 조합입니다:
correct-horse-battery-staple
violet-mountain-cloud-seven-river
7776단어 목록(Diceware)에서 각 단어는 약 12.9비트의 엔트로피를 추가합니다. 4단어 ~52비트, 5단어 ~64비트, 6단어 ~77비트입니다.
장점:
- 모바일에서 입력하기 더 쉬움
- 가끔 기억해야 할 때 더 기억하기 쉬움
- 화면에서 읽을 때 오독 가능성이 낮음
단점:
- 더 길어 타이핑이 많음
- 일부 시스템의 문자 제한에 걸릴 수 있음
길이 vs 복잡도
둘 중 하나를 선택해야 한다면 더 길게를 선택하세요. 문자 하나를 추가하면 탐색 공간이 풀 크기(26~94배)만큼 곱해집니다. 이미 소+대+숫자가 있을 때 특수문자를 추가하는 것은 생각보다 효과가 적습니다(16자 비밀번호에서 3비트 미만 추가).
실용적 권장 사항: 혼합 문자 클래스로 최소 16자 이상 사용하세요.
비밀번호는 외우면 안 됩니다
모든 계정에 고유한 무작위 비밀번호를 생성한다면(해야 합니다), 기억할 수 없습니다. 비밀번호 관리자가 존재하는 이유입니다.
비밀번호 관리자는 모든 비밀번호를 하나의 마스터 비밀번호로 암호화해 저장합니다. 강력한 패스프레이즈 하나만 기억하고 나머지는 관리자에게 맡기세요.
좋은 선택지:
- Bitwarden — 오픈소스, 무료 티어 우수, 자체 호스팅 가능
- 1Password — 세련된 UX, 강력한 팀 공유 기능
- KeePassXC — 로컬 전용, 클라우드 의존성 없음
비밀번호를 절대 재사용하지 마세요. 데이터 유출 하나가 재사용한 모든 계정을 노출시킵니다.
개발자용: 애플리케이션 시크릿 생성
API 키, JWT 시크릿, 세션 키, 암호화 키 같은 애플리케이션 시크릿에는 플랫폼의 보안 무작위 바이트 함수를 사용하세요:
# Shell — 32바이트 무작위를 16진수로 (대부분의 시크릿에 적합)
openssl rand -hex 32
# Shell — base64 인코딩
openssl rand -base64 32
import secrets
# 32바이트 16진수 시크릿 (64자)
api_key = secrets.token_hex(32)
# URL 안전 base64 시크릿
session_secret = secrets.token_urlsafe(32)
// Node.js
import { randomBytes } from 'crypto';
const apiKey = randomBytes(32).toString('hex');
JWT 시크릿에는 최소 256비트(32바이트)를 사용하세요. AES-256에는 정확히 32바이트가 필요합니다. 사람이 기억할 수 있는 문자열을 암호화 키로 절대 사용하지 마세요.
Have I Been Pwned 확인
Have I Been Pwned(HIBP) API를 사용하면 실제 비밀번호를 전송하지 않고도 비밀번호가 알려진 침해 데이터베이스에 있는지 확인할 수 있습니다:
- 비밀번호의 SHA-1 해시를 계산
- 해시의 앞 5자만 API에 전송
- API는 해당 5자로 시작하는 모든 해시를 반환
- 로컬에서 전체 해시가 목록에 있는지 확인
import hashlib
import requests
def is_pwned(password):
sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
response = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
hashes = (line.split(':') for line in response.text.splitlines())
return any(h == suffix for h, _ in hashes)
if is_pwned("P@ssw0rd1"):
print("이 비밀번호는 알려진 침해 데이터에 포함되어 있습니다!")
HIBP 데이터베이스에 있는 비밀번호는 겉보기 복잡도와 무관하게 절대 사용하면 안 됩니다.
요약
| 목표 | 권장 방법 |
|---|---|
| 무작위 비밀번호 | 16자 이상, 혼합 문자셋 |
| 기억 가능한 패스프레이즈 | Diceware에서 6개 이상의 무작위 단어 |
| 애플리케이션 시크릿 | secrets.token_hex(32) 또는 openssl rand -hex 32 |
| 비밀번호 저장 | Argon2id (최소 3회 반복, 64MB 메모리) |
| 침해 확인 | Have I Been Pwned API |
| 일상 비밀번호 관리 | Bitwarden 또는 1Password |
엔트로피, 무작위성, 고유성 — 임의적인 복잡도 규칙이 아닌 이것들이 비밀번호를 안전하게 만듭니다.