Base64는 암호화가 아니라 인코딩이다

Base64는 암호화가 아니라 인코딩이다

한눈에 보기

Base64는 바이너리를 ASCII 문자열로 운반하기 위한 인코딩이다. 기밀성, 무결성, 인증을 제공하지 않으며 데이터 크기도 약 33퍼센트 증가한다.

목차

왜 이 문제가 생기는가

문자열이 U2VjcmV0IQ==처럼 보이면 원문을 알아보기 어려워 안전해 보일 수 있다. 하지만 Base64는 키 없이 누구나 되돌릴 수 있는 표현 방식이다. 비밀번호나 개인정보를 Base64로 바꾸어 데이터베이스에 저장하는 것은 사실상 원문을 다른 문자로 적어 둔 것과 같다.

Base64의 목적은 비밀을 숨기는 것이 아니라 바이너리 바이트를 텍스트만 안전하게 다루는 채널로 운반하는 것이다. 이 목적을 정확히 알면 JWT payload, Basic 인증, Data URL을 볼 때 무엇이 보호되고 무엇이 그대로 노출되는지 구분할 수 있다.

이 글의 예제에 관하여

문자열과 토큰은 인코딩 원리를 설명하기 위해 만든 값이며 실제 자격 증명을 사용하지 않았다.

Base64가 해결하는 문제

이미지, 압축 파일, 암호화 결과는 임의의 바이트 값을 포함할 수 있다. 반면 오래된 텍스트 프로토콜이나 JSON 문자열, URL 일부는 제한된 문자 집합으로 표현할 때 다루기 쉽다. Base64는 입력 바이트를 64개의 안전한 문자로 매핑한다.

const bytes = Buffer.from([0xff, 0xd8, 0xff, 0xe0]);
const encoded = bytes.toString("base64");
const restored = Buffer.from(encoded, "base64");

console.log(encoded); // /9j/4A==
console.log(restored.equals(bytes)); // true

여기에는 비밀 키도 없고 복잡한 권한도 없다. 같은 입력은 같은 문자열이 되고 표준을 아는 누구나 원래 바이트를 복원한다. 이 가역성은 오류가 아니라 인코딩의 목적이다.

3바이트가 4문자로 바뀌는 원리

Base64는 입력을 24비트, 즉 3바이트씩 묶고 이를 6비트 네 덩어리로 나눈다. 6비트로 표현할 수 있는 값은 0부터 63까지 64개이므로 각 값을 문자 하나에 대응시킬 수 있다.

입력 3바이트
01001000 01101001 00100001

6비트씩 다시 나누기
010010 000110 100100 100001
  18      6      36     33

Base64 문자표에 대응
   S      G       k      h
const encoded = Buffer.from("Hi!", "utf8").toString("base64");
console.log(encoded); // SGkh

3바이트가 4문자로 바뀌므로 충분히 큰 입력에서는 크기가 대략 4/3이 된다. 여기에 JSON 따옴표, Data URL prefix, 전송 압축 등의 조건이 더해지므로 실제 네트워크 크기는 문맥에 따라 측정해야 한다.

function base64Length(byteLength) {
  return 4 * Math.ceil(byteLength / 3);
}

console.log(base64Length(3)); // 4
console.log(base64Length(4)); // 8

패딩 문자는 왜 필요한가

입력 길이가 3바이트의 배수가 아니면 마지막 24비트 묶음이 채워지지 않는다. Base64는 필요한 비트를 0으로 채우고 출력 길이를 4문자 단위로 맞추기 위해 = padding을 사용한다.

원문 바이트 수 Base64
M 1 TQ==
Ma 2 TWE=
Man 3 TWFu

padding은 암호화된 비밀이나 구분자가 아니다. 마지막 quantum에 실제 입력 바이트가 몇 개였는지 표현하는 형식의 일부다. 일부 문맥에서는 데이터 길이를 외부에서 알 수 있어 padding을 생략하기도 하지만, decoder가 해당 규칙을 지원하는지 확인해야 한다.

Base64URL은 무엇이 다른가

일반 Base64의 +, /, =는 URL query나 파일명 문맥에서 별도 인코딩이 필요할 수 있다. Base64URL은 문자 일부를 바꾸고 보통 padding을 생략해 URL에 넣기 쉬운 형태를 사용한다.

일반 Base64 Base64URL
+ -
/ _
= padding 사용 문맥에 따라 생략
const payload = Buffer.from(JSON.stringify({ sub: "user-42" }));

console.log(payload.toString("base64"));
console.log(payload.toString("base64url"));

Base64와 Base64URL을 이름만 보고 섞으면 특정 입력에서만 디코딩 오류가 발생한다. API 계약에 사용하는 alphabet과 padding 규칙을 명시한다.

암호화, 해시, 서명과 역할 비교

Base64를 다른 보안 기술과 비교하면 오해가 줄어든다.

기술 주된 목적 원문 복원
인코딩 표현 형식 변환 없음 누구나 가능
암호화 기밀성 암·복호화 키 올바른 키로 가능
암호학적 해시 무결성 비교·단방향 요약 보통 없음 원문 복원 목적 아님
MAC 무결성·공유 키 기반 인증 공유 비밀 키 서명값에서 원문 복원 안 함
디지털 서명 무결성·발급자 검증 개인키/공개키 서명값에서 원문 복원 안 함

비밀번호 저장에는 빠른 일반 해시 한 번이 아니라 salt와 비용 조절이 가능한 비밀번호 해싱 알고리즘을 사용한다. 복호화해야 하는 개인정보에는 검증된 인증 암호화 방식과 키 관리가 필요하다. 로그나 URL에서 값을 알아보기 어렵게 만들고 싶다는 이유만으로 Base64를 보안 경계로 사용하면 안 된다.

Basic 인증과 JWT에서 보이는 Base64

HTTP Basic 인증은 username:password를 Base64로 표현한다.

const credentials = Buffer.from("demo-user:demo-password", "utf8")
  .toString("base64");

console.log(`Authorization: Basic ${credentials}`);

네트워크를 보는 사람은 즉시 원문으로 복원할 수 있으므로 TLS 없이 사용하면 자격 증명이 노출된다. HTTPS를 사용해도 자격 증명의 수명, 로그 마스킹, brute-force 방어는 별도로 필요하다.

JWT의 header와 payload도 Base64URL로 표현되는 경우가 많다.

base64url(header).base64url(payload).signature

payload는 브라우저 콘솔이나 일반 decoder로 읽을 수 있다. 서명된 JWT는 내용 변조를 검증할 수 있지만 내용을 숨기는 암호문이 아니다. 자세한 검증 규칙은 JWT의 서명과 암호화를 혼동하면 안 되는 이유에서 이어서 다룬다.

이미지를 Base64로 넣을 때의 비용

작은 아이콘을 Data URL로 CSS나 HTML에 넣으면 별도 요청을 줄일 수 있지만 항상 성능이 좋아지는 것은 아니다.

<img
  alt="작은 예시 점"
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..."
/>

고려할 비용은 다음과 같다.

작은 자산 몇 개는 이점이 있을 수 있지만 큰 이미지나 사용자 업로드를 JSON Base64로 보내는 방식은 multipart 또는 직접 object storage 업로드와 비교한다.

디코딩은 검증이 아니다

Base64 디코딩에 성공했다는 사실은 데이터가 안전하거나 예상한 형식이라는 뜻이 아니다. decoder의 엄격성에 따라 잘못된 문자를 무시하거나 padding 차이를 허용할 수도 있다. 외부 입력을 처리할 때는 인코딩 형식뿐 아니라 디코딩된 바이트의 길이와 업무 스키마를 검증한다.

function decodeSmallToken(raw: string): Buffer {
  if (!/^[A-Za-z0-9_-]{1,256}$/.test(raw)) {
    throw new Error("INVALID_TOKEN_ENCODING");
  }

  const decoded = Buffer.from(raw, "base64url");
  if (decoded.byteLength > 192) {
    throw new Error("TOKEN_TOO_LARGE");
  }

  return decoded;
}

이 검사는 단지 입력 자원과 형식을 제한한다. 토큰의 진위, 만료, 권한은 서명 검증과 도메인 규칙으로 별도 확인해야 한다.

실전 점검 목록

Base64를 사용하기 전에

  • 목적이 보안이 아니라 텍스트 채널을 통한 바이트 운반인가?
  • 일반 Base64와 Base64URL 중 어느 규격인지 명확한가?
  • 약 33%의 크기 증가가 허용되는가?
  • decoded 결과의 크기와 구조를 다시 검증하는가?
  • 자격 증명과 개인정보를 숨기는 수단으로 오해하지 않는가?
  • 큰 바이너리는 multipart나 object storage가 더 적합하지 않은가?

Base64는 바이너리를 ASCII 문자열로 운반하기 위한 인코딩이다. 기밀성, 무결성, 인증을 제공하지 않으며 데이터 크기도 약 33퍼센트 증가한다.

결론

Base64는 임의의 바이트를 제한된 텍스트 문자로 표현하기 위한 가역 인코딩이며 기밀성, 무결성, 인증을 제공하지 않는다. 3바이트가 4문자로 바뀌는 크기 비용과 Base64URL·padding 규칙을 문맥에 맞게 확인한다. 비밀을 보호해야 한다면 목적에 맞는 암호화, 비밀번호 해싱, MAC 또는 서명을 선택하고 키와 검증 정책까지 설계해야 한다.

관련 노트