WebPiki
it

해시 알고리즘이란? SHA-256부터 비밀번호 저장까지

해시 함수의 원리, SHA 패밀리, 실무에서의 활용법과 보안 주의사항을 정리했다.

데이터를 고유 지문으로 변환하는 해시 함수

비밀번호를 데이터베이스에 저장할 때 평문 그대로 넣는 개발자는 없다 (있으면 안 된다). 해시 함수를 통과시켜서 원래 값을 알 수 없는 형태로 바꿔서 저장한다. 블록체인의 핵심 기술도 해시다. Git이 커밋을 식별하는 것도 해시. 파일 다운로드할 때 무결성 확인하는 것도 해시. 생각보다 여기저기 쓰인다.

해시 함수가 하는 일

입력 데이터를 고정 길이의 값으로 변환한다. 이 출력값을 해시, 다이제스트, 체크섬이라고 부른다.

핵심 성질이 몇 가지 있다:

결정성 — 같은 입력은 항상 같은 출력을 만든다. "hello"를 SHA-256에 넣으면 언제나 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824가 나온다.

단방향성 — 해시 값에서 원래 입력을 역산하는 건 (이론적으로) 불가능하다. 실용적인 의미에서 되돌릴 수 없다.

눈사태 효과 — 입력이 1비트만 바뀌어도 출력이 완전히 달라진다. "hello"와 "hellp"의 해시는 전혀 다르다.

충돌 저항성 — 서로 다른 두 입력이 같은 해시를 만드는 경우(충돌)를 찾기 극도로 어렵다.

SHA 패밀리

SHA(Secure Hash Algorithm)는 미국 국가안보국(NSA)이 설계하고 NIST가 표준화한 해시 함수 모음이다.

SHA-1

160비트(40자리 16진수) 출력. Git 커밋 해시가 SHA-1이다. 하지만 2017년에 구글이 충돌을 시연했다. 두 개의 다른 PDF 파일이 같은 SHA-1 해시를 갖도록 만든 거다. 이후로 보안 용도에서는 사용이 비권장된다.

SHA-256

256비트(64자리 16진수) 출력. 현재 가장 널리 쓰이는 해시 함수. 비트코인 채굴도 SHA-256 기반이다. SSL/TLS 인증서, 소프트웨어 서명 등 대부분의 보안 관련 해싱에 쓰인다.

SHA-384 / SHA-512

더 긴 출력. SHA-512는 64비트 프로세서에서 SHA-256보다 오히려 빠를 수 있다. 64비트 연산을 기반으로 설계되었기 때문.

SHA-3

Keccak 알고리즘 기반. SHA-2와는 완전히 다른 내부 구조를 가진다. SHA-2에 취약점이 발견될 경우를 대비한 백업 표준 같은 위치인데, SHA-2가 아직 안전해서 실사용은 드물다.

해시 vs 암호화

이 둘은 완전히 다른 개념이다. 헷갈리는 경우가 흔한데 명확히 구분해야 한다.

해시암호화
방향단방향 (복원 불가)양방향 (키로 복호화)
목적무결성 검증, 식별기밀성 보호
키 필요없음필요
예시SHA-256, bcryptAES, RSA

"해시로 암호화했다"는 표현은 기술적으로 틀렸다. 해시는 암호화가 아니다.

실무 활용

비밀번호 저장

비밀번호를 해시해서 저장하면, DB가 유출되어도 평문 비밀번호는 노출되지 않는다. 하지만 단순 SHA-256 해시만으로는 부족하다.

문제 1: 레인보우 테이블. "password123" → SHA-256 해시를 미리 계산해놓은 거대한 테이블이 있다. 흔한 비밀번호는 이 테이블에서 바로 찾을 수 있다.

해결: 솔트(Salt). 비밀번호에 랜덤 문자열을 붙인 뒤 해시한다. 같은 비밀번호라도 솔트가 다르면 해시가 달라진다.

문제 2: 속도. SHA-256은 빠르다. 그게 비밀번호에서는 단점이다. 공격자가 초당 수십억 개의 해시를 시도할 수 있다.

해결: bcrypt, scrypt, Argon2. 의도적으로 느리게 설계된 해시 함수. Argon2가 현재 권장. 비밀번호에는 SHA-256이 아니라 이런 전용 함수를 써야 한다. 절대 일반 해시 함수를 비밀번호에 쓰지 말 것.

파일 무결성 검증

소프트웨어를 다운로드할 때 제공되는 SHA-256 체크섬은 파일이 변조되지 않았음을 확인하기 위한 것이다. 다운로드한 파일의 해시를 계산해서 공식 해시와 비교한다.

# Linux/macOS
sha256sum downloaded-file.iso
# 또는
shasum -a 256 downloaded-file.iso

# Windows PowerShell
Get-FileHash downloaded-file.iso -Algorithm SHA256

패키지 매니저도 같은 원리를 쓴다. npm이 package-lock.jsonintegrity 필드로 SHA-512 해시를 저장하는 이유가 이거다. 누가 패키지를 변조하면 해시가 달라지니까 설치 시점에 잡아낸다.

Git

Git의 모든 객체(커밋, 트리, 블롭)는 SHA-1 해시로 식별된다. 커밋 해시 a1b2c3d는 그 커밋의 내용(변경사항, 작성자, 시간, 부모 커밋 등)을 SHA-1에 넣은 결과다. 내용이 1바이트라도 다르면 해시가 달라지니까, 커밋의 무결성이 보장된다.

# Git이 내부적으로 하는 일과 비슷한 방식
printf "blob 5\0hello" | sha1sum
# aaf4c61ddcc5e8a2dabede0f3b482cd9aea9434d

Git은 SHA-256으로 전환하는 작업을 진행 중이다. SHA-1 충돌 공격이 가능해졌기 때문. git init --object-format=sha256으로 이미 SHA-256 저장소를 만들 수 있긴 하지만, 아직 실험적 단계다.

블록체인

비트코인의 작업 증명(Proof of Work)은 SHA-256 해시의 앞부분이 특정 수의 0으로 시작하는 값을 찾는 과정이다. 해시의 특성상 입력을 조금씩 바꿔가며 무작위로 시도하는 수밖에 없다. 이게 채굴의 본질이고, 엄청난 계산 자원이 필요한 이유다.

HMAC

HMAC(Hash-based Message Authentication Code)은 해시와 비밀 키를 결합한 메시지 인증 코드다. API 요청이 변조되지 않았음을 확인할 때 사용한다. AWS API 서명, GitHub 웹훅 검증 등에서 HMAC-SHA256을 쓴다.

단순 해시와 HMAC의 차이를 짚어보자. SHA-256 해시만으로 메시지 무결성을 확인하려면, 해시 값과 메시지를 함께 보내야 한다. 문제는 중간에서 메시지를 바꾸고 해시도 다시 계산해서 보내면 수신 측이 변조를 알 수 없다는 점이다. HMAC은 비밀 키를 함께 사용하기 때문에 키를 모르는 공격자가 해시를 위조할 수 없다.

// Node.js에서 HMAC-SHA256 생성
const crypto = require("crypto");
const hmac = crypto.createHmac("sha256", "my-secret-key")
  .update("message-to-verify")
  .digest("hex");

GitHub 웹훅을 받으면 X-Hub-Signature-256 헤더에 HMAC이 들어있다. 이걸 서버에서 같은 시크릿으로 계산한 값과 비교해서, 요청이 진짜 GitHub에서 온 건지 확인한다.

비밀번호 해싱: bcrypt, scrypt, Argon2

앞에서 비밀번호에 SHA-256을 쓰면 안 된다고 했다. 그러면 뭘 써야 하나? 세 가지 선택지가 있다.

bcrypt — 1999년에 나왔는데 아직도 많이 쓴다. cost factor를 조정해서 해싱 속도를 느리게 만든다. 하드웨어가 좋아지면 cost를 올리면 된다. 문제는 입력 길이 제한(72바이트)이 있다는 것과, GPU 병렬 공격에 상대적으로 취약하다는 점이다.

scrypt — bcrypt의 약점을 보완하려고 2009년에 나왔다. CPU뿐 아니라 메모리도 많이 쓰도록 설계됐다. GPU는 연산은 빠르지만 메모리가 제한적이라서, 메모리를 많이 요구하면 병렬 공격 비용이 크게 올라간다.

Argon2 — 2015년 Password Hashing Competition 우승자. Argon2id가 권장 변형이다. CPU 사용량, 메모리 사용량, 병렬성 세 가지를 독립적으로 조절할 수 있어서 가장 유연하다. OWASP에서 신규 프로젝트에 권장하는 알고리즘이다.

bcryptscryptArgon2id
출시199920092015
메모리 사용낮음높음 (조절 가능)높음 (조절 가능)
GPU 저항성보통높음높음
설정 유연성cost factor만CPU + 메모리CPU + 메모리 + 병렬성

새 프로젝트라면 Argon2id를 쓰는 게 가장 안전한 선택이다. 기존에 bcrypt를 쓰고 있다면 당장 바꿀 필요까지는 없지만, cost factor가 충분히 높은지(최소 12 이상 권장)는 확인하자.

해시 충돌이 실제로 일어난 사례들

이론적으로 충돌이 가능하다는 건 알겠는데, 실제로 문제가 된 적이 있을까? 있다.

MD5 충돌 — 2012년 Flame 악성코드: 2012년에 발견된 Flame 악성코드는 MD5 충돌을 이용해 가짜 Microsoft 인증서를 만들었다. 정상적인 Windows 업데이트처럼 위장해서 악성코드를 배포한 사례다.

SHA-1 충돌 — 2017년 SHAttered: 구글과 CWI Amsterdam이 공동으로 SHA-1 충돌을 시연했다. 시각적으로 다른 두 PDF가 동일한 SHA-1 해시를 갖도록 만들었다. 9 x 10^18 번의 SHA-1 계산이 필요했고 비용이 약 11만 달러 수준이었는데, 국가 단위 공격자에게는 충분히 감당 가능한 수준이다.

이런 사례들이 SHA-256으로의 전환을 가속화했다. SHA-256에서 충돌을 찾으려면 현재 기술로는 우주의 나이보다 긴 시간이 필요하다고 추정된다. 물론 양자 컴퓨팅이 발전하면 얘기가 달라질 수 있다.

MD5는 왜 안 쓰나

MD5는 128비트 해시를 생성하는 오래된 함수다. 1991년에 나왔는데, 2004년에 충돌이 발견됐고, 이후 노트북 한 대로도 충돌을 만들 수 있게 됐다. 보안 용도로는 완전히 퇴출. 하지만 보안이 필요 없는 체크섬(빠른 파일 비교 등)에는 아직 쓰이기도 한다.

재밌는 건 MD5 충돌을 시각적으로 보여주는 사례도 있다는 거다. 서로 다른 두 이미지 파일인데 MD5 해시가 동일한 경우를 만들어낸 연구가 있었다. 실제로 보면 해시 충돌이 왜 위험한지 확 와닿는다.

코드로 해시 써보기

대부분의 언어에서 해시 계산은 몇 줄이면 된다.

// Node.js
const crypto = require("crypto");
const hash = crypto.createHash("sha256").update("hello").digest("hex");
// 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
# Python
import hashlib
h = hashlib.sha256(b"hello").hexdigest()
# CLI
echo -n "hello" | sha256sum

echo -n에서 -n이 중요하다. 이걸 빼먹으면 줄바꿈 문자가 포함돼서 전혀 다른 해시가 나온다. 은근히 자주 하는 실수.

브라우저에서 간단하게 해시를 확인하고 싶으면 해시 생성기를 쓸 수 있다. 텍스트를 입력하면 MD5, SHA-1, SHA-256, SHA-512를 한번에 볼 수 있다.

어떤 해시를 써야 하나

용도별로 정리하면 이렇다:

  • 비밀번호 저장: Argon2 또는 bcrypt. SHA 계열 쓰면 안 된다
  • 파일 무결성 검증: SHA-256이 표준. 속도가 중요하면 xxHash나 BLAKE3도 괜찮다
  • 데이터 구조(해시맵 등): 언어 런타임 기본 해시면 충분. 보안과 무관
  • 디지털 서명, 인증서: SHA-256 이상
  • 빠른 체크섬: CRC32, xxHash. 보안 필요 없는 경우에만

용도를 헷갈리면 사고가 난다. "해시는 다 같은 거 아냐?"라고 생각하는 순간 비밀번호를 SHA-256으로 저장하는 일이 벌어진다. 해시 알고리즘은 도구 상자 안의 도구다. 망치로 나사를 박지 말자.

#해시#SHA-256#보안#암호화#블록체인

관련 글