Design rationale
같은 문제를 푸는 알고리즘은 대개 여럿입니다. 무엇을 골랐는지보다 무엇을 고르지 않았고 왜인지가 더 많은 것을 알려 줍니다. 여기서 탈락한 선택지도 “나쁜 알고리즘”이 아니라 이 맥락에 맞지 않았을 뿐이며, 유리한 상황이 있으면 함께 적었습니다.
AES-256-GCM (대안으로 ChaCha20-Poly1305)
암호화와 무결성을 한 연산으로 묶는 AEAD만 쓴다.
기밀성만 있고 무결성이 없으면 공격자가 암호문을 조작해 평문을 바꿀 수 있습니다. 직접 조립하는 방식(암호화 후 MAC)은 순서를 한 번만 잘못 잡아도 무너지므로, 두 성질을 하나의 연산으로 제공하는 AEAD를 씁니다. 가속이 없는 환경을 위해 ChaCha20-Poly1305를 나란히 둡니다.
AES-CBC + HMAC 직접 조립
MAC-then-Encrypt 순서를 잘못 잡으면 패딩 오라클이 열립니다. 정확히 조립해도 검증 순서·상수 시간 비교를 직접 책임져야 해 실수 여지가 큽니다.
다만 — AEAD를 제공하지 않는 오래된 라이브러리와 상호운용해야 할 때.
AES-CBC 단독
무결성이 전혀 없습니다. 암호문 변조가 검출되지 않아 복호 결과를 신뢰할 수 없습니다.
AES-ECB
같은 평문 블록이 같은 암호문 블록이 되어 패턴이 그대로 드러납니다. 어떤 용도로도 권장되지 않습니다.
scrypt (N=32768, r=8, p=1)
연산뿐 아니라 메모리도 요구해 병렬 공격의 단가를 올린다.
비밀번호는 엔트로피가 낮아 공격자가 대량 추측을 시도합니다. 방어는 추측 한 번의 단가를 올리는 것뿐인데, 연산량만 올리면 GPU·ASIC이 대량 병렬화로 상쇄합니다. scrypt는 메모리를 함께 요구해 병렬도 자체를 제한합니다.
PBKDF2
반복 횟수만 올릴 뿐 메모리 요구가 고정이라 병렬 하드웨어에 약합니다.
다만 — FIPS 준수가 요구되는 환경. 검증된 구현의 폭이 가장 넓습니다.
argon2id
현재 가장 권장되는 선택이지만 Node 표준 라이브러리에 없어 네이티브 의존성이 필요합니다. 이 프로젝트는 런타임 기본 제공 프리미티브만 쓴다는 규율을 우선했습니다.
다만 — 네이티브 의존성을 감수할 수 있다면 지금은 argon2id가 1순위입니다.
bcrypt
비밀번호 길이가 72바이트에서 잘리고 메모리 비용을 조절할 수 없습니다.
SHA-256 단독
너무 빠른 것이 문제입니다. 초당 수십억 회 시도가 가능해 방어가 성립하지 않습니다.
HMAC-SHA-256
해시에 키를 그냥 이어 붙이는 방식은 MAC이 아니다.
SHA-256 같은 Merkle–Damgård 해시는 출력이 곧 내부 상태라, 키를 앞에 붙이는 방식은 뒤에 내용을 덧붙인 메시지의 MAC까지 계산당합니다. HMAC의 중첩 구조가 정확히 이 경로를 닫습니다.
SHA-256(key ‖ message)
길이 확장 공격으로 위조됩니다. 실제 서비스가 이 방식으로 뚫린 사례가 있습니다.
SHA-3 / BLAKE2 키 모드
구조상 길이 확장에 안전해 HMAC 없이도 됩니다. 다만 HMAC-SHA-256이 상호운용 폭이 넓어 공통 모듈에는 이쪽이 맞습니다.
다만 — 성능이 중요하고 상호운용 대상이 없을 때 BLAKE2는 더 빠릅니다.
CRC · 체크섬
전송 오류 검출용이며 의도적 변조를 막지 못합니다. 키가 없어 누구나 다시 계산할 수 있습니다.
ES256(호환) · Ed25519(신규 설계)
생태계가 요구하면 ES256, 새로 설계하는 경계는 Ed25519.
ECDSA는 서명마다 비밀 난수가 필요하고 그 값이 재사용되면 개인키가 복구됩니다. Ed25519는 그 값을 키와 메시지에서 계산해 실패 범주 자체를 없앱니다. 다만 WebAuthn 인증기와 대부분의 OIDC 공급자가 ES256/RS256을 요구하므로 둘을 함께 둡니다.
RS256 (RSA)
키와 서명이 훨씬 크고 키 생성이 느립니다. 패딩 방식(PKCS#1 v1.5)의 구현 실수 이력도 깁니다.
다만 — 구형 시스템과의 상호운용이 필요할 때. 여전히 가장 널리 지원됩니다.
ES256 단독
난수 품질에 의존하는 구조가 남습니다. RFC 6979(결정론적 ECDSA)로 완화할 수 있지만 구현 지원이 고르지 않습니다.
X25519
곡선 설계 자체가 구현 실수를 줄인다.
X25519는 특수 케이스 분기가 거의 없도록 설계되어 상수 시간 구현이 자연스럽고, 잘못된 공개키를 받아도 안전 실패로 끝납니다. 성능도 좋고 키가 32바이트로 짧습니다.
유한체 DH (FFDHE)
같은 안전 수준에 키가 훨씬 크고 느립니다. 약한 파라미터 협상으로 다운그레이드된 사례가 있습니다.
P-256 ECDH
널리 지원되지만 구현이 까다로워 무효 곡선 공격 등 실수 이력이 많습니다.
다만 — FIPS 준수가 요구되는 환경.
키를 서버 공개키로 감싸 전송(RSA 키 수송)
전방 비밀성이 없습니다. 나중에 서버 키가 유출되면 저장해 둔 과거 트래픽이 전부 복호됩니다. TLS 1.3에서 제거됐습니다.
HKDF-SHA-256
추출과 확장을 분리한 표준 유도를 쓴다.
용도마다 별도 키를 환경변수로 주입하게 하면 번거로워 결국 한 키를 돌려쓰게 됩니다. HKDF는 마스터 하나에서 용도별 키를 결정론적으로 유도해, 키 분리를 문서가 아니라 기본 동작으로 만듭니다.
SHA-256(master ‖ purpose)
구분자가 없으면 서로 다른 조합이 같은 입력으로 뭉개집니다. 길이 확장에도 노출됩니다.
용도별 키를 각각 환경변수로 주입
안전하지만 운영 부담이 커서 실무에서 지켜지지 않습니다. HKDF와 병용할 수 있습니다.
다만 — 키마다 서로 다른 HSM·KMS에 두어야 할 때.
timingSafeEqual (상수 시간)
비교에 걸린 시간도 답의 일부다.
일반 비교는 다른 바이트를 만나면 즉시 멈추고, 그 조기 종료가 앞에서 몇 바이트가 맞았는지를 흘립니다. 그 한 가지 정보만으로 전수 탐색이 길이에 비례하는 비용으로 줄어듭니다.
=== · Buffer.equals
조기 종료로 일치 접두사 길이가 새어 나갑니다. 8바이트 MAC이 2,048번 시도로 복원됩니다.
양쪽을 다시 HMAC해서 비교
유효한 완화책입니다. 비교 대상이 매번 달라져 접두사 정보가 무의미해집니다. 다만 연산이 하나 늘고 상수 시간 비교만큼 직관적이지 않습니다.
다만 — 상수 시간 비교 API가 없는 런타임.
정규형 base64url (왕복 검사)
같은 바이트는 반드시 같은 문자열이어야 한다.
관대한 디코더는 서로 다른 문자열을 같은 바이트로 받아들입니다. 그러면 문자열로 비교하는 곳과 바이트로 비교하는 곳의 판단이 갈라져, 사용 완료 토큰 목록 같은 장치가 우회됩니다. 디코딩 후 재인코딩이 원본과 같을 때만 통과시킵니다.
표준 base64
+ 와 / 가 URL·쿠키에서 인코딩되어야 하고 = 패딩이 붙습니다.
관대한 base64url 디코딩
"QQ"와 "QR"이 모두 통과합니다. 문자열 동일성과 바이트 동일성이 갈라집니다.
16진수
안전하지만 같은 데이터에 두 배 길이가 필요합니다. 토큰이 길어져 URL·쿠키에 부담입니다.
다만 — 사람이 눈으로 대조해야 하는 값(지문 등).
여기 적힌 근거는 각 모듈의 증명과 묶여 있습니다 — 가리키는 모듈이나 증명이 사라지면 CI가 실패하므로, 구현이 바뀌었는데 근거만 남는 상태가 생기지 않습니다.