2P by GN⁺ | ★ favorite | 댓글 1개
  • Base64 인코딩은 바이너리 데이터를 ASCII 텍스트로 바꿔, 저장·전송 과정에서 데이터가 잘못 해석될 가능성을 줄이는 방식임
  • 암호화가 아니라 표현 방식 변경이므로, 인코딩된 데이터는 원래 텍스트나 파일 데이터로 쉽게 되돌릴 수 있음
  • 64개 문자는 6비트로 표현 가능해 Base64 한 글자는 6비트 데이터를 담고, 3바이트(24비트)는 네 개의 Base64 문자로 바뀜
  • HTML의 Data URLs, 이메일의 바이너리 전송, 텍스트 중심 네트워크나 URL처럼 원시 바이너리가 문제를 일으킬 수 있는 환경에서 유용함
  • Ruby, C#, PHP, JavaScript, 터미널의 base64 명령 등 여러 언어와 도구가 인코딩·디코딩 기능을 제공함

Base64가 바꾸는 것

  • Base64 인코딩은 바이너리 데이터를 텍스트, 더 구체적으로 ASCII 텍스트로 변환함
  • 결과는 아래 64개 문자만 사용함
    • A-Z
    • a-z
    • 0-9
    • +
    • /
  • 이 문자 집합은 <, >, \n 같은 문자가 오래된 컴퓨터나 프로그램에서 잘못 해석되는 상황을 피하기 위한 안전한 문자 집합으로 쓰임
  • "Ruby on Rails"를 Base64로 인코딩하면 UnVieSBvbiBSYWlscw==가 됨
  • Base64는 암호화가 아님
    • 인코딩된 데이터는 원래 텍스트로 쉽게 되돌릴 수 있음
    • 데이터를 숨기지 않고 데이터의 표현만 바꿈

Base64를 쓰는 상황

  • Data URLs는 이미지 같은 파일 데이터를 HTML 안에 직접 넣을 수 있게 하며, 이때 Base64 인코딩된 텍스트를 사용함
  • 예시 형식은 data:[<mime type>][;charset=<charset>][;base64],<encoded data>
  • 이메일에서는 서버가 줄바꿈을 바꿀 수 있는 환경에서도 바이너리 데이터를 안전하게 담기 위해 Base64가 쓰여 왔음
  • HTML 소스에 이미지 데이터를 직접 넣을 때는 <> 같은 문자가 태그로 해석되지 않도록 인코딩이 필요함
  • 텍스트 또는 US-ASCII 데이터를 처리하도록 설계된 네트워크를 통해 바이너리 데이터를 저장하거나 전송할 때도 사용할 수 있음
  • URL에 넣기 어려운 문자가 포함된 데이터를 전달할 때도 Base64가 쓰일 수 있음
  • Base 계열 인코딩은 객체를 텍스트 편집기로 다룰 수 있게 해 여러 애플리케이션에서 활용됨

인코딩 알고리듬

  • Base64 인코딩은 다음 순서로 진행됨
    • 텍스트를 바이너리 표현으로 변환함
    • 비트를 6비트씩 나눔
    • 각 6비트 그룹을 0부터 63까지의 십진수로 변환함
    • 이 숫자를 Base64 알파벳의 대응 문자로 바꿈
  • 마지막 그룹의 비트가 부족하면 = 또는 ==패딩으로 붙일 수 있음
  • 64개 문자를 표현하려면 6비트가 필요함
    • 2^6 = 64
    • Base64 숫자 하나는 6비트 데이터를 나타냄
  • 바이트는 8비트이고, 8과 6의 가장 가까운 공배수는 24임
    • 24비트는 3바이트임
    • 24비트는 네 개의 6비트 Base64 숫자로 표현됨

“Akshay” 인코딩 예시

  • "Akshay"는 각 문자를 ASCII 숫자로 바꾼 뒤 바이너리로 변환하면 다음과 같음
    • 01000001 01101011 01110011 01101000 01100001 01111001
  • 이를 6비트 그룹으로 나누면 다음과 같음
    • 010000 010110 101101 110011 011010 000110 000101 111001
  • 각 그룹을 십진수로 바꾸면 다음 값을 얻음
    • 16 22 45 51 26 6 5 57
  • Base64 알파벳으로 변환하면 다음 문자가 됨
    • Q W t z a G F 5
  • 따라서 "Akshay"의 Base64 표현은 QWtzaGF5
  • 같은 방식으로 이미지, PDF, 텍스트, 비디오 같은 파일도 바이너리로 바꾼 뒤 Base64로 인코딩하면 ASCII 텍스트로 저장하거나 전송할 수 있음

언어와 도구에서의 사용

  • Ruby는 Base64 모듈로 인코딩과 디코딩을 처리함
    • Base64.encode64("Ruby on Rails")
    • Base64.decode64(encoded)
  • C#은 문자열을 바이트 배열로 바꾼 뒤 Convert.ToBase64String으로 인코딩하고, System.Convert.FromBase64String으로 디코딩함
  • PHP는 최상위 함수인 base64_encodebase64_decode를 제공함
  • JavaScript는 btoa()로 인코딩하고 atob()로 디코딩함
  • 터미널에서도 base64 명령으로 인코딩과 디코딩을 수행할 수 있음
    • echo "akshay" | base64YWtzaGF5Cg==를 출력함
    • echo "YWtzaGF5Cg==" | base64 -dakshay를 출력함

댓글과 토론

Hacker News 의견들
  • 여기서 텍스트를 암호화하는 게 아니라는 점을 강조해줘서 고마움. 많은 주니어 개발자가 암호화는 비밀값이 있어야 되돌릴 수 있고, 해싱은 되돌릴 수 없으며, 인코딩은 항상 쉽게 되돌릴 수 있다는 차이를 너무 늦게 배워서 당하곤 함
    출력이 무작위처럼 보여도 엔트로피는 입력과 같다는 점도 알아둘 만함. 즉 비밀번호를 더 강하게 만들려고 Base64로 인코딩하면 안 됨

    • 꼬투리 잡기에 가깝고 핵심과는 별 상관 없지만, 비밀번호를 Base64로 인코딩하면 강해질 수도 있음. 비밀번호 강도는 엔트로피만의 문제가 아니고, 높은 엔트로피가 가장 효과적인 방법일 뿐임
      비밀번호가 완전히 무작위로 생성된 것이라면 Base64 인코딩은 아무 효과가 없음. 하지만 사전 단어나 기억하기 쉬운 규칙처럼 낮은 엔트로피 체계로 만든 비밀번호라면, 공격자가 똑똑한 비밀번호 크래커에 Base64 인코딩 규칙까지 고려하도록 설정해야 하므로 시도마다 연산 하나가 더 붙는 정도의 강도는 추가됨
      물론 이런 비밀번호 체계를 쓰면 안 됨. “correct horse battery staple”류의 비밀번호면 충분하다고 봄
    • 전산학 전공자라면 이런 차이는 다 알 거라고 생각함. 코딩에 관심 있는 사람도 오후 한나절이면 배울 수 있는 개념들임
    • 관련해서 항상 강조할 만한 점은, 해시가 반드시 암호학적으로 안전한 것은 아니라는 것
      해싱은 보안 말고도 목적이 많고, 그래서 해시 라이브러리도 다양함. 보안이나 암호 관련 용도로 해시를 쓸 거라면 그 목적에 맞게 설계된 해시를 써야 함. CRC 해시는 빠르긴 하지만, 사용자 비밀번호에 쓰기에는 좋지 않음
  • Base64의 재미있는 점 하나는, 어떤 문자열에서 시작해 인코딩을 반복하면 결과의 앞부분이 점점 고정점으로 수렴한다는 것임. Bash로도 확인 가능함
    10년도 더 전에 우연히 발견하고 암호처럼 트윗했는데 [1], 누군가 그 주제로 블로그 글을 썼고 여기에도 올렸지만 토론은 별로 없었음 [2]. 다른 사람이 Reddit /r/compsci에 올리자 그쪽에서는 블로그 글을 바로잡는 생산적인 논의가 있었음 [3]. 지금 블로그는 내려갔지만 인터넷 아카이브에 사본이 남아 있음 [4]
    [1] https://twitter.com/p4bl0/status/298900842076045312
    [2] https://news.ycombinator.com/item?id=5181256
    [3] https://www.reddit.com/r/compsci/comments/18234a/the_base64_...
    [4] https://web.archive.org/web/20130315082932/http://fmota.eu/b...

  • Bash로 인코딩할 때는 -n 옵션을 써야 함: $ echo -n "abcde" |base64
    -n이 없으면 echo가 문자열 끝에 개행 문자를 하나 추가하고, 그 문자까지 인코딩됨

  • base64URL도 있는데, URL에 안전한 다른 ASCII 문자를 사용해 인코딩함. 어떤 개발자들은 BASE64URL을 그냥 base64라고 부르기도 해서, 모르는 사람에게 문제가 생길 수 있음
    https://datatracker.ietf.org/doc/html/rfc4648#section-5

    • base64url의 문제는 ~.이 글자가 아니라서, 인코딩된 값을 더블클릭해도 전체가 선택되지 않는다는 점임. 복사-붙여넣기하려는 많은 경우에 불필요한 마찰이 됨
      Base62 인코딩(0-9A-Za-z)은 base64url과 거의 비슷하게 효율적이고 URL 안전성도 유지하면서 복사-붙여넣기가 더 쉬움. 사람이 읽을 때의 모호성을 줄이고 싶다면 Base58로 내려갈 수 있지만, 보통 BaseXX 인코딩을 할 정도면 길이가 길어 복사-붙여넣기가 일반적이라 큰 문제는 아님
      https://en.wikipedia.org/wiki/Base62
    • Base64url은 보통 패딩도 생략함
      패딩이 있는 Base64 문자열은 항상 길이가 4의 배수이므로, 4의 배수가 아닌 문자열을 받으면 원래 패딩이 얼마나 있어야 했는지 알 수 있고 마지막 3바이트를 어떻게 디코딩할지도 판단할 수 있음
      그래서 Base64에 애초에 == 패딩이 왜 필요한지 조금 헷갈림
  • 진법 변환 얘기가 나올 때마다 임의 진법 변환기를 뻔뻔하게 홍보함: https://convert.zamicol.com
    “useful alphabets” 아래의 base64는 기수로 반복 나눗셈을 하는 “자연스러운” 진법이고, RFC의 “버킷” 변환 방식은 extras 아래에 있음

  • 무언가를 인코딩했는데 사람이 직접 타이핑해야 한다면 https://en.wikipedia.org/wiki/Base32를 추천함
    나쁜 글꼴 때문에 l인지 1인지, o인지 O인지 0인지 헷갈리는 것만큼 짜증나는 일이 없음

  • 조금 엄밀히 말하면, Base64는 바이너리 데이터를 ASCII 문자 전체가 아니라 ASCII 부분집합으로 인코딩한다고 하는 편이 더 정확함
    ASCII에는 128개 코드 포인트가 있고, 그중 출력 가능한 문자가 95개, 제어 문자가 33개인데 Base64는 그중 64개, 패딩까지 포함하면 65개만 사용함

  • 글에서 = / == 패딩의 목적을 자세히 다루지 않았고, 6비트 그룹으로 딱 나누어지지 않는 데이터를 어떻게 처리하는지도 예제로 보여주지 않았음
    대략 이해는 한 것 같지만 확실히 알고 싶음. =는 언제 쓰고 ==는 언제 쓰는지, 항상 추가하는지 아니면 안 붙이는 경우도 있는지, "5byte" 같은 문자열의 남는 비트는 정확히 어떻게 처리하는지, 디코딩할 때 고려할 점이 있는지 짧고 완전하게 답해줄 수 있으면 좋겠음

    • 두 질문은 서로 연결되어 있음
      Base64 문자 하나는 6비트를 나타내므로, 데이터 3바이트 블록 하나는 Base64 인코딩 문자 4개 블록에 대응함. 그래서 Base64 데이터는 4문자씩 처리하기 편함
      =는 인코딩된 문자열 길이를 4의 배수로 맞추기 위해 필요에 따라 0개, 1개, 2개를 붙이는 패딩임. 예를 들어 "543210""543210==", "6543210""6543210=", "76543210"은 패딩이 필요 없음. 패딩으로 =가 3개 필요할 일은 없는데, 데이터 1바이트도 최소 Base64 문자 2개가 필요하기 때문임
      남는 비트는 0으로 채우면 되고, 디코더는 완전한 1바이트를 만들 만큼 비트가 충분하지 않다는 걸 보고 버릴 수 있음. 현대적인 대부분의 경우 패딩은 꼭 필요하다기보다 관례에 가까움. Wikipedia 글이 꽤 자세함: https://en.wikipedia.org/wiki/Base64
    • 패딩은 인코딩된 데이터를 이어붙이거나 스트리밍할 때, 즉 인코딩 스트림 중간에 패딩 문자가 들어가는 경우에만 필요함
      스트림·파일·문자열의 끝에 있는 패딩 문자는 이미 처리한 길이로 추론할 수 있으므로 엄밀히는 필수 아님
      다만 패딩 처리 방식은 꽤 미묘하고, 그 차이 때문에 흥미로운 구현 변형들이 생겼음: https://eprint.iacr.org/2022/361.pdf
    • 글에 따르면 Base64 숫자 하나는 데이터 6비트를 나타냄. 바이트는 8비트이고, 8과 6의 가장 가까운 공배수는 24이므로 24비트, 즉 3바이트를 6비트 Base64 숫자 4개로 표현할 수 있음
      결국 24비트 단위로 인코딩하는 셈임. 데이터가 끝나면 남은 24비트 부분을 A가 아니라 =로 채우는데, A는 데이터로서 000000을 뜻하기 때문임. 나도 이해하려고 전체를 두 번 읽었음
  • Base64 인코더 셰이더는 여기 있음: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
    GLSL 약 13줄까지 줄였음: https://github.com/Rezmason/excel_97_egg/blob/main/glsl/base...
    사이드 프로젝트의 Cursed Mode에서 쓰는데, WebGL 프레임버퍼를 Base64로 인코딩된 640x480 픽셀 인덱스 컬러 BMP로 초당 약 15번 렌더링함: https://rezmason.github.io/excel_97_egg/?cursed=1

  • 깊게 파고들기 시작하면 추가로 흥미로운 세부사항이 있고, 그 세부사항의 변형도 놀랄 만큼 많음
    입력 데이터 길이가 정확히 3바이트의 배수가 아니면, 마지막 1바이트 또는 2바이트를 인코딩하는 데 Base64 문자 2개 또는 3개를 씀. Base64 문자 하나가 6비트이므로 8비트나 16비트를 표현하려고 12비트나 18비트를 쓰게 되고, 결과적으로 아무것도 인코딩하지 않는 여분 4비트 또는 2비트가 생김
    RFC에서는 인코더가 그 비트들을 0으로 설정해야 한다고 요구하지만, 디코더는 그 비트가 0이 아닌 입력을 거부할 수 있다고만 되어 있음. 실제로는 기본값으로 거부하는 구현이 거의 없고, 내가 알기로 Ruby, Rust, Go 정도만 그런 입력에서 실패하도록 설정할 수 있음. Python에는 validate 옵션이 있지만 그 비트들은 검증하지 않음
    또 다른 큰 차이는 공백과 Base64가 아닌 문자 처리임. Python을 포함한 의외로 많은 구현이 입력의 임의 문자를 조용히 무시함. 알파벳을 잘못 고르면 문제가 되는데, 예를 들어 Python에서 base64.standard_b64decode(base64.urlsafe_b64encode(b'\xFF\xFE\xFD\xFC'))는 오류를 내지 않고 조용히 잘못된 출력을 줌
    Ruby의 Base64 인코더가 60자마다 줄바꿈을 넣는 것도 재미있음. PEM 말고는 그렇게 짧은 줄을 요구하는 표준 인코딩이 없고, PEM은 정확히 64자 줄을 요구하니 꽤 특이한 선택임
    프로그래밍 언어와 일부 JavaScript 라이브러리 간 차이를 정리한 글을 썼고 [1], 더 나은 Base64를 JS에 추가하려는 작업도 하고 있음 [2]
    [1] https://gist.github.com/bakkot/16cae276209da91b652c2cb3f612a...
    [2] https://github.com/tc39/proposal-arraybuffer-base64