개발자를 위한 암호화 기초 — 해시, 대칭키, 비대칭키
bcrypt, AES, RSA/ECDSA 등 실무에서 쓰는 암호화 방식의 원리와 적용법.
한 줄 요약: 비밀번호는 bcrypt로 해시, 데이터는 AES로 암호화, 서명과 키 교환은 RSA/ECDSA — 각 도구가 해결하는 문제가 다르다. 상황에 맞는 암호화를 써야 한다.
암호화를 잘 몰라도 개발은 가능하다. 하지만 잘못된 암호화는 아예 안 하는 것보다 더 위험하다. MD5로 비밀번호를 해시하거나 ECB 모드로 AES를 쓰는 것처럼 — 겉으로는 암호화처럼 보이지만 실제로는 무방비 상태다. 이 글은 해시, 대칭키, 비대칭키 암호화를 개발자 관점에서 실용적으로 정리한다.
해시 함수 — 비밀번호는 이렇게 저장한다
해시는 단방향 함수다. 입력에서 고정 길이 출력을 만들고, 출력에서 원래 입력을 복원할 수 없다. 비밀번호 저장에 적합하다. 데이터를 복호화할 필요가 없고, 검증만 하면 되기 때문이다.
해시 알고리즘 선택 기준
| 알고리즘 | 용도 | 비밀번호 저장 | 비고 |
|---|---|---|---|
| MD5 | 파일 무결성(레거시) | 절대 사용 금지 | 충돌 취약, GPU 초당 수십억 해시 |
| SHA-256 | 데이터 무결성, JWT 서명 | 단독 사용 금지 | 빠름 → 레인보우 테이블 취약 |
| bcrypt | 비밀번호 저장 | 권장 | salt 자동 포함, cost factor 조정 가능 |
| Argon2id | 비밀번호 저장 | 최신 권장 | 메모리 사용 → GPU 공격 어렵게 설계 |
| PBKDF2 | 비밀번호 저장, 키 유도 | 허용 | 반복 횟수 충분히 설정 필요(600,000+) |
bcrypt와 Argon2 비밀번호 해시 예시 (Node.js)const bcrypt = require('bcrypt'); const argon2 = require('argon2'); // bcrypt — cost factor 12 권장 (2026년 기준) async function hashPasswordBcrypt(password) { const saltRounds = 12; // 값이 클수록 느려짐 (2^12 반복) return await bcrypt.hash(password, saltRounds); } async function verifyPasswordBcrypt(password, hash) { return await bcrypt.compare(password, hash); // hash 내부에 salt가 포함되어 있어 별도 저장 불필요 } // Argon2id — 현재 OWASP 최우선 권장 async function hashPasswordArgon2(password) { return await argon2.hash(password, { type: argon2.argon2id, memoryCost: 64 * 1024, // 64MB timeCost: 3, // 반복 횟수 parallelism: 4 }); } async function verifyPasswordArgon2(password, hash) { return await argon2.verify(hash, password); } // 사용 const hash = await hashPasswordArgon2(req.body.password); await db.query('INSERT INTO users (email, password_hash) VALUES ($1, $2)', [email, hash]);
대칭키 암호화 — AES로 데이터를 잠근다
대칭키 암호화는 같은 키로 암호화와 복호화를 모두 한다. 속도가 빠르고 대용량 데이터에 적합하다. AES(Advanced Encryption Standard)가 현재 표준이다.
AES 모드 선택
| 모드 | 특징 | 권장 여부 |
|---|---|---|
| ECB | 블록 독립 암호화 — 같은 평문 = 같은 암호문 | 절대 사용 금지 |
| CBC | IV(초기화 벡터) 필요, 패딩 필요 | 조건부 허용 (GCM 권장) |
| GCM | 인증 포함 (AEAD), IV 필요 | 현재 표준 권장 |
| ChaCha20-Poly1305 | 모바일/임베디드 최적화 | 권장 (AES 가속 없는 환경) |
AES-256-GCM을 쓰면 암호화와 무결성 검증(변조 탐지)을 동시에 처리한다. CBC 모드는 패딩 오라클 공격에 취약하므로 GCM을 우선한다.
AES-256-GCM 암호화/복호화 예시 (Node.js crypto)const crypto = require('crypto'); const ALGORITHM = 'aes-256-gcm'; const KEY_LENGTH = 32; // 256 bits // 키 생성 (환경 변수에서 로드하거나 KMS 사용) // const key = crypto.randomBytes(KEY_LENGTH); // 생성 예시 const key = Buffer.from(process.env.ENCRYPTION_KEY, 'hex'); function encrypt(plaintext) { const iv = crypto.randomBytes(12); // GCM에는 12바이트 IV 권장 const cipher = crypto.createCipheriv(ALGORITHM, key, iv); let encrypted = cipher.update(plaintext, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag(); // iv + authTag + ciphertext를 함께 저장 return { iv: iv.toString('hex'), authTag: authTag.toString('hex'), data: encrypted }; } function decrypt(encryptedObj) { const { iv, authTag, data } = encryptedObj; const decipher = crypto.createDecipheriv( ALGORITHM, key, Buffer.from(iv, 'hex') ); decipher.setAuthTag(Buffer.from(authTag, 'hex')); let decrypted = decipher.update(data, 'hex', 'utf8'); decrypted += decipher.final('utf8'); // authTag 검증 실패 시 여기서 에러 return decrypted; }
비대칭키 암호화 — RSA와 ECDSA의 역할
비대칭키 암호화는 공개키(Public Key)와 비밀키(Private Key)를 사용한다. 공개키로 암호화하면 비밀키로만 복호화 가능하고, 비밀키로 서명하면 공개키로 검증 가능하다.
RSA vs ECDSA 비교
| 항목 | RSA | ECDSA |
|---|---|---|
| 키 길이 | 2048~4096 bits | 256~384 bits (동급 보안) |
| 속도 | 상대적으로 느림 | 빠름 |
| 키 크기 | 큼 | 작음 (모바일 유리) |
| 주요 용도 | TLS 키 교환, 암호화 | 디지털 서명, JWT, TLS |
| 호환성 | 광범위 | 최신 환경 권장 |
실무 사용 시나리오
- JWT 서명: 단일 서버라면 HS256(대칭, HMAC-SHA256). 마이크로서비스처럼 여러 서비스가 검증해야 하면 RS256 또는 ES256(비대칭)이 적합 — 검증 서비스에 비밀키를 배포하지 않아도 됨.
- 데이터 암호화 + 전송: 대용량 데이터는 AES-256-GCM으로 암호화하고, AES 키 자체를 RSA로 암호화하는 하이브리드 방식 사용.
- 코드 서명: 배포 아티팩트의 무결성 검증에 ECDSA 사용.
TLS — 전송 구간 보안의 기반
TLS(Transport Layer Security)는 앞서 설명한 암호화 기법들을 조합해 네트워크 전송을 보호한다. HTTPS의 보안 계층이 TLS다.
TLS 핸드셰이크 간략 흐름
- 클라이언트가 지원 가능한 암호화 스위트 목록 전송
- 서버가 인증서(공개키 포함) 전송 + 암호화 스위트 선택
- 클라이언트가 인증서 유효성 확인 (CA 서명 검증)
- 키 교환 — ECDH로 세션 키(대칭키) 협상
- 이후 통신은 협상된 대칭키(AES-GCM)로 암호화
개발자가 확인해야 할 TLS 설정
- TLS 1.2 이상만 허용 — TLS 1.0, 1.1, SSL 비활성화
- 약한 암호화 스위트 비활성화 — RC4, DES, 3DES, MD5 기반 스위트 제거
- 인증서 체인 완전성 확인 — 중간 CA 인증서가 누락되면 일부 클라이언트에서 실패
- OCSP Stapling 활성화 — 인증서 상태 확인 속도 향상
Node.js HTTPS 서버 설정 + TLS 버전 제한const https = require('https'); const fs = require('fs'); const tls = require('tls'); const options = { key: fs.readFileSync('/path/to/privkey.pem'), cert: fs.readFileSync('/path/to/fullchain.pem'), minVersion: 'TLSv1.2', // TLS 1.2 이상만 허용 ciphers: [ 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', 'TLS_AES_128_GCM_SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-GCM-SHA256' ].join(':'), honorCipherOrder: true }; https.createServer(options, app).listen(443);
실무 적용 가이드 — 상황별 암호화 선택
어떤 암호화를 써야 하는지 상황별로 정리한다.
| 상황 | 권장 방법 | 비고 |
|---|---|---|
| 비밀번호 저장 | Argon2id 또는 bcrypt(cost 12+) | SHA-256 단독 절대 금지 |
| 개인정보 DB 저장 | AES-256-GCM | 키는 KMS 관리 |
| JWT 서명 (단일 서버) | HS256 (HMAC-SHA256) | 비밀키 32바이트 이상 |
| JWT 서명 (마이크로서비스) | RS256 또는 ES256 | 공개키만 배포, 비밀키 중앙 관리 |
| 파일 무결성 확인 | SHA-256 또는 SHA-3 | MD5 사용 금지 |
| API 키 생성 | crypto.randomBytes(32) | Math.random() 절대 금지 |
| 네트워크 전송 보안 | TLS 1.3 + AES-256-GCM | 인증서 자동 갱신 설정 |
암호화 라이브러리는 직접 구현하지 않는다. 검증된 라이브러리(Node.js 기본 crypto 모듈, libsodium, OpenSSL 기반 라이브러리)를 사용한다. 직접 구현한 암호화 알고리즘은 미묘한 버그 하나가 전체 보안을 무너뜨린다.