it-box

인코딩 해시 암호화 HMAC 차이와 쓰임

인코딩, 해시, 암호화, HMAC의 차이를 되돌릴 수 있는지 기준으로 정리하고, 언제 무엇을 쓰는지와 흔한 오해를 실제 계산 예제로 설명합니다.

네 단어가 섞여 쓰이는 이유

보안 관련 회의나 코드 리뷰에서 "암호화했습니다"라는 말이 나왔는데, 실제로는 Base64 인코딩만 한 경우가 자주 있습니다. 반대로 "해시로 저장합니다"라는 말이 나왔는데 실제로는 복호화가 가능한 AES로 저장한 경우도 있습니다. 용어가 섞이는 이유는 네 가지 작업이 모두 "원래 값을 다른 값으로 바꾼다"는 겉모습을 가지고 있기 때문입니다.

이 글에서는 네 가지를 한 가지 기준으로 나눠 봅니다. 기준은 세 가지입니다.

  1. 키(비밀 값)가 필요한가?
  2. 결과로부터 원래 값을 되돌릴 수 있는가?
  3. 같은 입력이면 항상 같은 결과가 나오는가?

이 기준으로 보면 인코딩, 해시, 암호화, HMAC은 서로 다른 자리를 차지합니다.

구분 키 필요 되돌리기 같은 입력에 같은 결과 대표 예
인코딩 없음 누구나 가능 항상 같음 Base64, URL 인코딩
해시 없음 원리상 불가능 항상 같음 SHA-256, MD5
암호화 있음 키가 있으면 가능 키와 무작위 값에 따라 다름 AES
HMAC 있음 원리상 불가능 같은 키면 같음 HMAC-SHA256

인코딩: 형식을 바꿀 뿐 보호하지 않는다

인코딩은 데이터를 다른 표현으로 옮기는 작업입니다. 목적은 전송이나 저장 경로에서 문제가 생기지 않게 하는 것입니다. Base64는 바이너리를 텍스트로 바꾸고, URL 인코딩은 URL에서 위험한 문자를 바꿉니다. 둘 다 규칙만 알면 누구나 되돌릴 수 있습니다.

"hello" → Base64 → aGVsbG8=
aGVsbG8= → Base64 디코딩 → "hello"

아무 키도 필요하지 않습니다. 그래서 비밀번호를 Base64로 바꿔 저장하면, 저장소를 본 사람은 원래 비밀번호를 바로 읽을 수 있습니다. Base64 변환기에 붙여 넣는 것만으로 끝납니다. 인코딩은 보호 수단이 아니며, 민감한 값을 감추려고 쓰면 안 됩니다.

Base64 인코딩의 세부 규칙은 Base64와 URL 인코딩 차이 글에서 다뤘습니다. 여기서는 "되돌릴 수 있다"는 점만 기억하면 충분합니다.

해시: 한 방향 요약 값

해시 함수는 임의 길이의 입력을 고정 길이의 출력으로 바꿉니다. SHA-256의 출력은 항상 256비트(64자리 16진수)입니다. 해시 텍스트 도구에서 같은 문자열을 여러 번 넣으면 같은 해시가 나옵니다. 이 도구는 MD5, SHA-1, SHA-2 계열(SHA-224, SHA-256, SHA-384, SHA-512), SHA-3, RIPEMD-160을 지원하고, 결과를 Hex, Base64 등 여러 형식으로 보여 줍니다.

실제 값을 보겠습니다.

SHA-256("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
MD5("hello")     = 5d41402abc4b2a76b9719d911017c592
SHA-256("hello\n") = 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03

hello와 hello\n(끝에 줄바꿈이 붙은 문자열)은 한 글자 차이입니다. 그래도 해시는 완전히 달라집니다. 파일 해시를 비교할 때 텍스트 끝의 줄바꿈 문자가 원인이 되는 경우가 자주 있습니다. 에디터가 파일 끝에 줄바꿈을 자동으로 넣었는지 먼저 확인해야 합니다.

해시가 되돌릴 수 없다고 말하는 이유는 함수의 출력 공간이 입력 공간보다 훨씬 작기 때문입니다. 같은 해시를 주는 다른 입력이 존재할 수 있습니다. 실제로 MD5와 SHA-1은 의도적으로 같은 해시를 만드는 충돌 공격이 발표되었습니다. 따라서 이 두 알고리즘은 새 시스템에서 무결성 검증이나 보안 목적으로 쓰지 않는 편이 맞습니다. 파일의 실수로 인한 변경을 확인하는 정도의 용도로는 여전히 쓰이지만, 공격자가 있는 환경에서는 SHA-256 이상을 쓰는 것이 일반적입니다.

해시를 비밀번호 저장에 그대로 쓰면 안 되는 이유는 별도의 글에서 다룹니다. 짧게 말하면 빠른 해시는 무차별 대입 공격에 너무 빠르기 때문입니다.

암호화: 키로 열고 닫는 양방향 변환

암호화는 키를 가진 사람만 원래 값을 되돌릴 수 있게 만드는 작업입니다. 대칭 키 암호(AES 등)는 같은 키로 암호화와 복호화를 모두 합니다.

암호화 도구는 AES, TripleDES, Rabbit, RC4를 제공합니다. 소스를 보면 이 도구는 crypto-js 라이브러리의 encrypt와 decrypt를 비밀 키 문자열과 함께 호출합니다. 키를 16진수 바이트로 직접 넣는 방식이 아니라, 문자열을 넘기면 라이브러리가 내부에서 키와 솔트를 처리하는 방식입니다.

도구의 복호화 예시 입력을 보겠습니다.

암호문: U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs
비밀 키: my secret key
알고리즘: AES
결과: Lorem ipsum dolor sit amet

암호문이 U2FsdGVkX1로 시작하는 이유는 이 형식이 Salted__라는 8바이트 머리말을 가지기 때문입니다. Salted__를 Base64로 바꾸면 U2FsdGVkX18=이 되고, 이 값의 앞부분이 U2FsdGVkX1입니다. 머리말 뒤에는 무작위 솔트가 붙습니다. 그래서 같은 텍스트를 같은 키로 두 번 암호화해도 결과가 달라집니다. 실제로 Lorem ipsum dolor sit amet를 my secret key로 두 번 암호화하면 암호문이 서로 다르고, 두 암호문은 모두 같은 원문으로 복호화됩니다.

다른 키를 넣으면 어떻게 될까요? 같은 암호문을 wrong으로 복호화하면 예외가 발생하지 않고 빈 문자열이 나왔습니다. 도구의 computedCatch는 예외가 발생했을 때만 오류 메시지를 세팅합니다. 따라서 키가 틀려서 결과가 빈 값이면 오류 메시지 없이 빈 칸만 보일 수 있습니다. 복호화 결과가 비어 있으면 키가 틀렸을 가능성을 먼저 봐야 합니다.

암호화에서 흔한 실수는 다음과 같습니다.

  • 키를 소스 코드에 하드코딩하는 경우입니다. my secret key 같은 값이 저장소에 있으면 암호화의 의미가 없습니다.
  • RC4처럼 오래되었거나 취약하다고 알려진 알고리즘을 새로 쓰는 경우입니다.
  • 암호화만 하고 무결성 검사를 하지 않는 경우입니다. 공격자가 암호문의 일부를 바꿔도 복호화 결과가 조용히 바뀔 수 있습니다. 이 때문에 인증된 암호(AEAD) 방식이나 뒤에 설명할 HMAC을 함께 씁니다.

HMAC: 키가 있는 해시

HMAC은 해시 함수와 비밀 키를 결합해서 메시지 인증 코드를 만듭니다. 입력은 메시지와 키이고, 출력은 고정 길이 값입니다. 키를 모르는 사람은 같은 값을 만들 수 없습니다. 그래서 HMAC은 "이 메시지를 키를 가진 쪽이 만들었고, 중간에 바뀌지 않았다"를 확인하는 데 씁니다.

HMAC 생성기는 MD5, SHA-1, SHA-2 계열, SHA-3, RIPEMD-160을 지원합니다. 소스를 보면 평문과 비밀 키를 받아 선택한 알고리즘으로 HMAC을 계산합니다.

예제 값을 보겠습니다.

HMAC-SHA256(key = "secret", message = "hello")
  = 88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b

HMAC-SHA256(key = "Secret", message = "hello")
  = dee977e188d4b0ef...   (앞 16자리만 표시)

키의 대소문자만 바꿔도 결과가 완전히 달라집니다. 이 성질 때문에 HMAC은 웹훅 서명이나 API 요청 서명에서 많이 씁니다. 발신자는 본문에 대해 HMAC을 계산해서 헤더에 넣고, 수신자는 같은 키로 다시 계산해서 비교합니다.

실무에서 HMAC을 검증할 때 주의할 점은 두 가지입니다.

  • 비교를 일반 문자열 비교(===)로 하면 응답 시간 차이로 서명 값을 조금씩 알아낼 수 있습니다. 상수 시간 비교 함수를 사용해야 합니다.
  • 서명 대상이 되는 본문 바이트를 그대로 써야 합니다. JSON을 파싱한 뒤 다시 직렬화하면 공백과 키 순서가 달라져서 검증이 실패할 수 있습니다.

언제 무엇을 쓰는가

요구 사항별로 정리하면 다음과 같습니다.

  • 파일이나 데이터를 텍스트로 옮겨야 한다: 인코딩(Base64)을 쓰고, 보호 목적은 없다고 생각합니다.
  • 파일이 중간에 바뀌지 않았는지만 확인하면 된다: SHA-256 해시를 씁니다. 공격자가 있다면 해시만으로는 부족하고, 서명된 값이나 HMAC이 필요합니다.
  • 나중에 원래 값을 꺼내 써야 한다: 암호화(AES 등)를 씁니다. 키 관리 계획을 먼저 세웁니다.
  • 외부에서 온 요청이 우리 쪽 키로 만든 것인지 확인해야 한다: HMAC을 씁니다.
  • 사용자 비밀번호를 저장해야 한다: 해시가 아니라 bcrypt, scrypt, Argon2 같은 비밀번호 전용 해시를 씁니다. 이 부분은 별도 글에서 다룹니다.

흔한 오해 정리

  • "Base64로 바꿨으니 안전하다." 아닙니다. 누구나 바로 되돌릴 수 있습니다.
  • "해시니까 복호화만 어렵다고 생각하면 된다." 해시는 복호화 자체가 정의되지 않습니다. 대신 같은 입력을 찾아내는 공격이 가능한지를 따져야 합니다.
  • "암호화했으니 변조도 막힌다." 암호화는 기밀성을 주지만 변조 탐지는 따로 설계해야 합니다.
  • "HMAC도 해시니까 키 없이 검증할 수 있다." 검증에는 같은 키가 필요합니다. 키가 없으면 HMAC 값을 만들 수 없습니다.
  • "MD5로 해시하면 충분히 빠르고 안전하다." 빠르다는 점이 비밀번호에서는 오히려 약점입니다. 충돌 공격도 알려져 있어 보안 목적에는 맞지 않습니다.

점검 목록

  • 이 값은 되돌려야 하는가? 그렇다면 키가 있는 암호화를 쓰고, 아니라면 해시나 HMAC을 씁니다.
  • 키는 어디에 저장되는가? 소스 코드, 저장소, 로그에 남아 있지 않은지 확인합니다.
  • 해시 비교를 할 때 줄바꿈이나 인코딩 차이가 없는지 확인합니다.
  • 복호화 결과가 비어 있는 경우를 실패로 처리하는가?
  • HMAC 검증에서 상수 시간 비교를 쓰는가?
  • 개인정보(이름, 전화번호, 주민등록번호)를 해시만으로 감출 수 있다고 생각하고 있지는 않은가? 주민등록번호처럼 입력 공간이 좁은 값은 해시해도 전수 조사가 가능합니다.

마지막 항목은 특히 자주 틀리는 부분입니다. 주민등록번호는 생년월일 6자리, 성별과 세기를 나타내는 1자리, 나머지 6자리로 이루어집니다. 생년월일과 성별 자리를 알면 남은 숫자는 10만 가지 수준입니다. 이런 값을 해시해서 저장해도 공격자가 후보를 모두 계산해 보면 원래 값을 알아낼 수 있습니다. 화면 표시나 로그에는 마스킹한 값(예: 900101-1******)만 쓰고, 원본은 필요한 최소한의 기간 동안만 보관하는 것이 맞습니다.

관련 도구