목차
지금까지는 글자를 바이트로 담았다. 이번에는 반대다. 바이트를 글자로 담아야 하는 상황이 있다.
글자만 통과시키는 검문소
어떤 길목은 텍스트만 지나갈 수 있다. 짐을 그대로 들고 가면 검문소에서 막힌다. 그럴 때 짐을 뜯어 글자 봉투에 나눠 담아 통과시키고, 반대편에서 다시 조립한다.
컴퓨터 세계에도 그런 길목이 여럿이다.
- 이메일 본문 - 원래 영문 텍스트만 나르도록 설계됐다. 첨부 파일은 그래서 글자로 바꿔 실린다
- JSON 문자열 - 값이 텍스트로 오가는 형식이라 이미지 같은 바이너리를 그대로 못 넣는다
- URL - 공백이나
&같은 글자는 주소 안에서 다른 뜻으로 읽힌다 - HTTP 헤더 - 줄바꿈이 헤더 경계라, 값에 줄바꿈이 있으면 구조가 무너진다
공통점은 하나다. 어떤 바이트는 그 길에서 다른 뜻으로 해석된다. 그래서 안전한 글자로 바꿔 실어야 한다.
3바이트를 4글자로 바꾼다
Base64가 하는 일은 단순하다. 바이트를 6비트씩 끊어 읽고, 각 조각을 안전한 글자 하나로 바꾼다.
비트는 하나도 안 늘었다. 늘어난 것은 6비트짜리 그릇에 담느라 필요해진 칸 수다.
핵심은 끊는 자리가 어긋난다는 것이다. 원래 데이터는 8비트씩 묶여 있는데 6비트씩 끊으니 경계가 안 맞는다. 그래서 최소 공배수인 24비트, 즉 3바이트를 한 덩어리로 잡아 4조각으로 나눈다.
6비트로 표현할 수 있는 값은 64가지다. 여기에 AZ, az, 0~9, 그리고 기호 둘을 배정한 것이 이름의 유래다. 전부 어디서나 안전한 글자들이다.
3바이트로 안 떨어지면 남는 자리를 =로 채운다. 문자열 끝의 =이나 ==가 그 흔적이다.
그래서 33% 커진다
8비트짜리 정보를 6비트 그릇에 담으니 그릇이 더 필요하다. 3바이트가 4글자가 되므로 정확히 3분의 1이 늘어난다. 1MB 이미지를 Base64로 만들면 약 1.33MB가 된다.
이게 실무에서 중요한 이유는 눈에 잘 안 띄기 때문이다. 이미지를 JSON에 담아 보내는 API는 잘 돌아가는데 전송량만 33% 더 쓴다. 게다가 이런 문자열은 대개 압축도 잘 안 먹는다.
Base64는 암호가 아니다
가장 흔한 오해다. Base64는 숨기지 않는다. 키도 없고 비밀도 없다. 규칙이 공개되어 있으니 누구나 원래대로 되돌린다.
YWRtaW46cGFzc3dvcmQ= → admin:password읽기 어려워 보이는 건 사람 눈에만 그렇다. 브라우저 콘솔 한 줄이면 풀린다.
그런데도 오해가 계속되는 건 비밀스러워 보이는 자리에 자주 등장하기 때문이다. 인증 헤더의 값, 토큰의 몸통, 쿠키에 담긴 데이터가 다 이렇게 생겼다. 하지만 이것들이 안전한 이유는 Base64 때문이 아니라 서명이나 암호화가 따로 되어 있어서다.
Base64는 포장이지 자물쇠가 아니다. 상자를 예쁘게 쌌다고 잠긴 건 아니다.
URL에서는 다른 포장을 쓴다
URL에도 안전하지 않은 글자가 있는데, 여기서는 Base64가 아니라 다른 방식을 쓴다. 문제 되는 글자만 골라 % 뒤에 16진수로 적는 것이다.
문제 되는 글자만 바꾸기 때문에 주소의 나머지는 사람이 읽을 수 있게 남는다. 대신 바뀐 자리는 눈에 띄게 길어진다.
%20이 공백이라는 건 여기서 나온다. 공백의 아스키 번호가 16진수로 20이다.
방식이 다른 이유는 목적이 달라서다. Base64는 전부 바꾼다. 퍼센트 인코딩은 문제 되는 것만 바꾼다. 그래서 URL은 대부분 사람이 읽을 수 있는 상태로 남는다.
한글은 안전한 글자가 아니라서 전부 바뀐다. UTF-8로 3바이트니 한 글자가 %XX%XX%XX 아홉 자가 된다. 주소창에서는 한글로 보여도 실제로 전송되는 건 이 모양이다.
두 번 포장하면 벗기기 어려워진다
여기서 실무 사고가 하나 나온다. 이미 인코딩된 값을 또 인코딩하는 것이다.
% 자체도 안전한 글자가 아니라서 인코딩 대상이다. 그래서 %20을 한 번 더 인코딩하면 %2520이 된다. 받는 쪽이 한 번만 디코딩하면 %20이라는 글자 그대로가 남는다.
증상은 이렇게 나타난다.
- 검색어에
%20이 글자로 박혀 나온다 - 리다이렉트 주소가 이상하게 늘어난다
- 파라미터에 URL을 담아 넘길 때만 깨진다
원인은 대개 프레임워크가 이미 해 준 일을 손으로 한 번 더 한 것이다. 어느 층이 인코딩을 책임지는지 정해 두는 게 유일한 예방이다.
실무에서: 어디에 쓰고 어디에 안 쓰나
쓸 만한 자리
- 작은 이미지를 문서에 심을 때 -
data:URI로 넣으면 요청이 한 번 준다. 아이콘처럼 작은 것에만 값이 있다. - 텍스트 통로에 바이너리를 실을 때 - 이메일 첨부가 원조이고, 지금도 JSON에 파일을 담을 때 쓴다.
- 헤더에 값을 실을 때 - 헤더는 줄바꿈에 민감해서 임의의 바이트를 그대로 넣으면 안 된다.
쓰면 안 되는 자리
- 큰 파일 - 33% 손해에 메모리까지 통째로 잡아먹는다. 파일은 파일대로 올리는 게 맞다.
- 숨기는 용도 - 앞에서 본 그대로다. 아무것도 안 숨긴다.
- 이미 안전한 것을 또 - 평범한 텍스트를 Base64로 감싸면 크기만 늘고 읽기만 어려워진다.
정리
- 어떤 길목은 텍스트만 통과시킨다. 그래서 바이트를 글자로 포장한다.
- Base64는 6비트씩 끊어 안전한 글자로 바꾼다. 8비트와 경계가 안 맞아 3바이트가 4글자가 된다.
- 그래서 크기가 33% 늘어난다. 눈에 안 띄는 비용이라 더 조심한다.
- Base64는 암호가 아니다. 누구나 되돌린다. 안전한 것들은 서명이나 암호화가 따로 되어 있어서 안전한 것이다.
- URL은 문제 되는 글자만
%XX로 바꾼다. 그래서 사람이 읽을 수 있는 부분이 남는다. - **두 번 인코딩하면
%2520**이 된다. 어느 층이 책임지는지 정해 두는 게 유일한 예방이다.
다음 글은 이 시리즈를 닫는다. 글자가 깨졌을 때 어디부터 의심할지를 한 장으로 정리한다.