Base64 형식을 다루어야 하나요? 그러면 여러분에게 이 웹사이트가 딱 맞네요! 저희 웹사이트의 아주 편리한 온라인 도구를 사용하여 데이터를 인코딩하거나 디코딩해보세요.

C에서의 Base64 인코딩: 완전한 가이드

당신에게 바이트가 있습니다. 디스크에서 읽은 JPEG일 수도, 토큰일 수도, 클라이언트가 곧 전송할 패스워드일 수도, 어떤 파이프라인이 JSON 필드 안에 기대하는 파일의 원본 바이트일 수도 있죠. 그리고 앞길 어딘가에는 텍스트만 말하는 채널이 있습니다: JSON 문자열, URL, 이메일 본문, 설정 파일, 텍스트인 척하는 데이터베이스 컬럼. 여기서 Base64 인코딩이 나섭니다: 원본 데이터 3바이트마다 64문자 알파벳의 4문자로 다시 쓰므로, 결과는 지구상의 어떤 텍스트 파이프라인도 무사히 통과하는 평문 ASCII가 됩니다. 이 사이트의 홈 페이지는 포맷을 온전히 설명해 줍니다. 이 기사는 C에서 그 일을 잘하는 법에 관한 것이고, 거기서는 - 늘 그렇듯이 - 언어가 대신해 주지 않습니다.

머릿속에 남길 숫자 하나: 인코딩은 당신의 데이터를 키웁니다. 3바이트가 4문자가 되므로, 모든 페이로드는 프로그램에서 떠나면서 약 3분의 1 커지고, 줄바꿈이 추가될 때마다 조금 더 커집니다. 이것이 세금이며, 그 돈을 피하는 길은 없습니다 - 하지만 C에서는 이 세금에 내역 항목이 있습니다. 출력 버퍼를 당신이 직접 할당하고, 버퍼는 정확히 딱 맞게 커야 하니까요. 계산을 한 번만 제대로 하면, 이 기사의 모든 인코더가 예측 가능해집니다: 오버플로도, 언더플로도, 다음 바이트가 어디에 떨어질지 궁금해하는 일도 없으니까요. 그리고 고를 도구상자가 있습니다 - OpenSSL, Mbed TLS, APR-Util, GLib - 선택은 중요합니다. 각각 래핑이 다르고, 종료 방식이 다르고, 오류 방식도 다르기 때문이죠.

인코더 넷, 성격 넷

네 라이브러리 모두 표준 알파벳을 정확하고 동일하게 인코딩합니다 - 들어가는 바이트가 같으면 나오는 문자도 언제나 같습니다. 차이는 포장이며, 상호 운용성 버그는 바로 그 포장 속에 숨어 있습니다. 전반상이 여기 있습니다:

라이브러리 헤더 출력 스타일 실패 방식
OpenSSL (libcrypto) <openssl/evp.h> 줄바꿈 없음; NUL 터미네이터를 쓴다 실질적으로 없음 (할당 실패만)
Mbed TLS <mbedtls/base64.h> 줄바꿈 없음; NUL 종료 필요 크기를 담은 버퍼-너무-작음 코드
APR-Util <apr-1.0/apr_base64.h> 줄바꿈 없음; NUL을 덧붙인다 없음 - 버퍼 크기를 믿으세요
GLib <glib.h> 줄바꿈 없음; NUL 종료, 힙 할당 NULL 반환 (할당 실패만)

표에서 빠져 있는 것에 주목하세요: 아무도 기본적으로 줄바꿈을 하지 않습니다. 이는 의도된 것입니다 - RFC 4648은 주변 사양이 명시적으로 요구하지 않는 한 구현이 라인 피드를 추가해서는 안 된다고 말하고 - 안심이 되는 일입니다. JSON 문자열이나 URL 안에 끼어들 줄바꿈은 기능이 아니라 오류이니까요. 래핑은 이메일과 PEM을 위해 존재하며, 필요할 때는 OpenSSL의 스트리밍 경로에서 받거나, 5줄로 스스로 래핑하면 됩니다(이메일 섹션에서 둘 다 보여 드립니다). 라이브러리 선택 기준: 이미 링크하고 있다면 OpenSSL, 킬로바이트 하나하나를 두고 다투는 임베디드 빌드는 Mbed TLS, Apache 생태계 안은 APR-Util, 프로그램의 나머지가 이미 GLib라면 GLib입니다. 설치: libssl-dev (Debian/Ubuntu) 또는 openssl-devel (Fedora/RHEL) 또는 brew install openssl (macOS); Mbed TLS는 libmbedtls-dev; APR-Util은 libaprutil1-dev와 libapr1-dev; GLib은 glib2.0-dev.

할당 전에 계산을 먼저

코드에 앞서, 산술부터 합니다: C는 버퍼가 너무 작은 것에서 당신을 구해 주지 않으니까요. 입력 바이트 3개마다 출력 문자가 정확히 4개 나옵니다. 입력 길이가 3의 배수가 아니라면, 마지막 그룹도 4문자를 만들며, 쓰지 않은 자리는 = 패드로 표시됩니다: 입력 1바이트는 패드 두 개를 달고 4문자가, 입력 2바이트는 패드 하나를 달고 4문자가 됩니다. 그래서 n바이트의 정확한 인코딩 길이는:

size_t encoded_chars(size_t n) {
  return ((n + 2) / 3) * 4;
}

1000바이트면 1336문자, 1바이트면 4, 0이면 0입니다. 여기서 두 가지 조정 사항이 따라옵니다. 첫째, OpenSSL과 Mbed TLS는 둘 다 데이터 뒤에 NUL 터미네이터를 붙입니다(Mbed TLS는 크기를 물어보면 그 자리를 미리 확보해 줍니다). 그래서 버퍼는 바이트 하나 더 필요합니다: encoded_chars(n) + 1. 둘째, 줄바꿈이 있는 출력을 원한다면 줄마다 줄바꿈 하나를 더하세요: OpenSSL의 스트리밍 인코더는 입력 48바이트마다 64문자 줄을 내뿜으므로, 래핑된 길이는 encoded_chars(n) + (n + 47) / 48가 됩니다. 1000바이트로 확인해 보세요: 1336문자에 줄바꿈 21개, 1357. 인코더가 정확히 만드는 것이 바로 그것입니다. 공식을 함수 하나로 써 두고 어디에나 쓰세요. 그것은 "들어맞는다"와 "새벽 3시의 힙 손상" 사이의 차이입니다.

size_t b64_buffer_size(size_t in_len) {
  return ((in_len + 2) / 3) * 4 + 1; /* 문자 수 + NUL */
}
size_t b64_buffer_size_wrapped(size_t in_len) {
  return ((in_len + 2) / 3) * 4 + (in_len + 47) / 48 + 1;
}

OpenSSL: 한 블록인가, 흐르는 수도꼭지가 아닌가

OpenSSL의 원샷 함수가 주력이며, 이 무리 중 가장 친절합니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *text = "Mane";
  unsigned char out[32];
  int n = EVP_EncodeBlock(out, (const unsigned char *)text,
                          (int)strlen(text));
  printf("len=%d str=%s\n", n, (char *)out);
  return 0;
}

인코딩된 문자를 out에 쓰고, 그 뒤에 NUL을 덧붙이며, NUL을 제외한 길이를 돌려줍니다 - 그래서 %s 출력은 안전하고, 필요하면 길이가 준비되어 있습니다. 출력 버퍼는 encoded_chars(n) + 1바이트를 담을 수 있어야 합니다. 처리할 오류 경로가 없습니다: 인코딩은 실패할 수 없습니다. 어떤 바이트든 합법적인 입력이므로, 걸려 넘어질 입력 검증 개념조차 이 함수에는 없으니까요. 잘못될 수 있는 유일한 길은 버퍼를 너무 작게 주는 것뿐이며, 산술 섹션이 바로 해독제입니다.

스트리밍 쌍은 데이터가 크거나 조각 조각 도착할 때 위한 것입니다. EVP_EncodeUpdate는 입력을 48바이트 블록 단위로 처리하고, 완성된 블록마다 64문자에 줄바꿈(65바이트)을 써내며, 나머지는 컨텍스트에 담아 더 많은 데이터나 최종 호출까지 기다립니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  if (ctx == NULL) {
    return 1;
  }
  EVP_EncodeInit(ctx);
  unsigned char in[1000];
  for (int i = 0; i < 1000; i++) {
    in[i] = (unsigned char)(i % 251);
  }
  unsigned char out[1400]; /* 문자 1336개 + 줄바꿈 21개 + 여유 공간 */
  int outl = 0;
  int total = 0;
  EVP_EncodeUpdate(ctx, out + total, &outl, in, 1000);
  total += outl;
  EVP_EncodeFinal(ctx, out + total, &outl);
  total += outl;
  int nl = 0;
  for (int i = 0; i < total; i++) {
    if (out[i] == '\n') nl++;
  }
  printf("encoded 1000 bytes into %d chars, %d newlines\n",
      total, nl);
  EVP_ENCODE_CTX_free(ctx);
  return 0;
}

그 프로그램의 출력은 줄바꿈 21개 들어간 1357바이트입니다 - 산술 섹션의 공식이 실물로 등장한 것이죠. 실용 노트 두 개. 버전 노트: OpenSSL 1.1.0 (2016) 이후 컨텍스트 타입은 불투명해졌으므로, EVP_ENCODE_CTX_new()로 할당하고 EVP_ENCODE_CTX_free()로 해제하세요. 1.0.2 이전을 대상으로 하는 튜토리얼에서 볼 수 있는 낡은 스택 패턴 EVP_ENCODE_CTX ctx;는 OpenSSL 3.x를 포함한 현대 헤더에는 컴파일되지 않습니다. 그리고 설계 노트: EVP_EncodeUpdate에서 나오는 것은 완성된 48바이트 블록뿐이므로, 가장 깨끗한 청크 파이프라인은 48의 배수를 먹입니다 - 그러면 함수가 쓰는 모든 줄이 완성된 줄이 되고, 꼬리를 어떻게 래핑할지는 EVP_EncodeFinal 혼자 결정합니다. 입력이 아무 크기로나 도착한다면(네트워크 읽기), 컨텍스트가 정렬을 대신 처리해 줍니다. 48의 배수라는 습관은 그저 출력을 예측 가능하게 해 주는 것뿐이죠.

Mbed TLS: 물어보고, 인코딩하고, 문자열을 얻다

Mbed TLS의 인코딩은 이 무리 중 가장 깨끗한 계약을 지니며, NULL 목적지로 호출할 수 있는 크기 조회를 중심으로 만들어져 있습니다:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/base64.h>
int main(void) {
  const char *text = "Mane";
  size_t slen = strlen(text);
  size_t needed = 0;
  int rc = mbedtls_base64_encode(NULL, 0, &needed,
      (const unsigned char *)text, slen);
  if (rc != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
    printf("size query failed: %d\n", rc);
    return 1;
  }
  printf("needs %zu bytes\n", needed);
  unsigned char *out = malloc(needed);
  size_t olen = 0;
  rc = mbedtls_base64_encode(out, needed, &olen,
      (const unsigned char *)text, slen);
  if (rc != 0) {
    printf("encode failed: %d\n", rc);
    free(out);
    return 1;
  }
  printf("olen=%zu str=%s\n", olen, out);
  free(out);
  return 0;
}

세부 사항을 꼼꼼히 읽어 보세요: 친절한 API의 마스터클래스이니까요. 크기 조회는 needed를 인코딩된 문자 수에 NUL 하나를 더한 값으로 보고합니다 - "Mane"의 경우 8에 1을 더해 9 - 그리고 NULL 목적지는 정의상 너무 작으므로, "버퍼가 너무 작음" 코드(MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL, 이는 -0x002A)로 스스로를 알립니다. 진짜 호출은 그 뒤 문자와 NUL을 쓰며, *olen은 터미네이터를 제외한 길이인 8로 돌아옵니다 - 그래서 버퍼는 이미 출력 가능한 C 문자열입니다. 바이트 하나 작은 버퍼를 넘기면, *olen에 필요한 크기를 담은 같은 "너무 작음" 코드가 돌아오므로, 실패가 정확히 얼마나 모자랐는지 알려 줍니다. 노트 하나 더: 이 라이브러리는 상수 시간 헬퍼로 테이블 조회를 하며, 대부분의 인코더에서는 볼 수 없는 작은 세심함입니다.

APR-Util과 GLib: 나머지 둘

APR-Util의 인코더는 int 길이를 쓰는 소박한 함수 쌍입니다:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <apr-1.0/apr_base64.h>
int main(void) {
  const char *text = "Mane";
  int needed = apr_base64_encode_len((int)strlen(text));
  char *out = malloc((size_t)needed);
  int n = apr_base64_encode(out, text, (int)strlen(text));
  printf("n=%d (includes NUL) str=%s\n", n, out);
  free(out);
  return 0;
}

여기서 apr_base64_encode_len()도, 반환 값도 NUL을 함께 세므로, n은 문자 수보다 하나 큽니다 - OpenSSL과 Mbed TLS와의 집계 방식 차이로, 여러 코드베이스에서 오프 바이 원 버그를 만들어 낸 바로 그 차이입니다. 같은 32비트 길이 한계도 적용됩니다: 2 GB 근처나 그 이상의 값에는 이 도구가 아닙니다. 또한 apr_base64_encode_binary()도 있는데, EBCDIC 머신에서는 입력의 EBCDIC-ASCII 변환을 건너뛕니다 - 그 변환이 일어나게 될 메인프레임에서는, 그리고 모든 다른 곳에서는 아무 효과도 없는 차이입니다. 이 쌍, 바이너리 변형, 그리고 짝을 이룬 디코딩 함수 - 이 것이 바로 apr-util이 드러내는 base64 표면 전체입니다: 풀 할당 변형도, 스트리밍 변형도 없으므로, 위 malloc 패턴이 유일한 패턴입니다.

GLib의 인코더는 힙 할당 스타일입니다 - NUL 종료 문자열을 주며, 의무도 하나 줍니다:

#include <stdio.h>
#include <glib.h>
int main(void) {
  const char *text = "Mane";
  gchar *enc = g_base64_encode((const guchar *)text, strlen(text));
  printf("%s\n", enc);
  g_free(enc);
  return 0;
}

래핑도 없고, NUL 종료이며, g_free로 해제합니다 - 프로토타입 위의 G_GNUC_MALLOC 어노테이션이 정적 분석기에게 그것을 알려 주는 것입니다. 줄바꿈이 정말 필요할 때는, 단계적 쌍이 그 도구입니다: g_base64_encode_step()은 상태 정수와 break_lines 플래그를 받아 몇 바이트의 출력을 썼는지 알려 주며, g_base64_encode_close()는 마지막 불완전 그룹을 마무리합니다. OpenSSL의 스트리밍 쌍과 같은 상태 머신 모양이며, 파라미터 스타일만 GLib식이죠.

손수 만든 URL-Safe Base64

위 네 라이브러리는 모두 표준 알파벳을 말합니다: A-Z, a-z, 0-9, 플러스, 슬래시. 그런데 웹은 점점 RFC 4648 5절의 두 번째 변형, base64url을 말하고 있습니다: +를 -로, /를 _로 바꾼 같은 인코딩에, 길이가 이미 알려져 있으면 꼬리의 = 패딩을 뺀 것입니다. JSON Web Token, OAuth state 파라미터, 무수한 API ID가 이것을 씁니다: +와 /는 URL에서는 둘 다 위험하지만, -와 _는 비예약 문자라 막힘없이 지나가니까요. 어떤 C 라이브러리도 이 변형을 네이티브로 내뱉지 않으므로, 스스로 만들어야 합니다 - 그리고 두 가지 작은 변경이면 됩니다. 표준 알파벳 인코더가 이미 있으니까요:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
static void to_base64url(const char *std_b64, char *out, size_t out_cap) {
  size_t i = 0;
  for (const char *p = std_b64; *p && *p != '='; p++) {
    char c = *p;
    if (c == '+') c = '-';
    if (c == '/') c = '_';
    out[i++] = c;
  }
  out[i] = '\0'; /* 패딩은 고의로 제거 */
}

사용은 두 단계 - 표준으로 인코딩하고, 번역하면 됩니다:

unsigned char enc[32];
EVP_EncodeBlock(enc, (const unsigned char *)"hi>there", 8);
char url_safe[32];
to_base64url((const char *)enc, url_safe, sizeof(url_safe));
printf("%s\n", url_safe); /* aGk-dGhlcmU */

주의 두 가지. 루프는 첫 =에서 멈추는데, 그것이 패딩을 버리는 동작입니다 - 그것을 "고치려" 하지 마세요. 패딩을 버리는 것이 목적입니다 (필요한 수신자는 길이에서 다시 더할 수 있으니까). 그리고 out은 더 적은 수가 아니라 인코딩된 전체 길이로 크기 잡으세요: 번역은 패드까지 문자 그대로 1:1이므로, 표준 형태를 위해 이미 할당해 둔 용량이 딱 맞습니다. 상호 운용성을 위한 정직한 노트 하나: 데이터에 우연히 +나 /로 매핑되는 바이트가 하나도 없다면, 표준 형태와 URL-safe 형태는 동일하고, 혼동에 대해 아무도 불평하지 않을 것입니다 - 버그가 드러나는 것은 데이터가 마침내 그런 문자를 하나 포함하게 될 때입니다. 변형을 데이터의 속성이 아니라 채널(URL, 토큰)의 속성으로 다루세요.

텍스트와 캐릭터셋: UTF-8은 그냥 바이트

C 신참을 놀라게 하는 질문: 악센트가 있는 텍스트, 이모지, CJK 문자는 어떻게 되나요? 답은 이 기사에서 가장 해방감을 주는 사실입니다 - 아무 일도 일어나지 않아도 됩니다. Base64는 바이트 위에서 일하고, C는 바이트의 언어입니다. 텍스트가 UTF-8라면(2026년이니 그럴 가능성이 큽니다), "café"의 UTF-8 인코딩은 5바이트 - 63 61 66 c3 a9 - 이며, Base64는 그 5바이트를 다른 어떤 5바이트와 똑같이 인코딩해 Y2Fmw6k=를 만듭니다. 캐릭터셋 파라미터도, BOM도, 변환 단계도, 라이브러리 호출도 없죠. 코덱은 그 바이트가 무슨 뜻인지 모르고, 알 일도 없습니다. 그것이 설계 전체입니다.

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *utf8 = "caf\303\251"; /* café의 UTF-8 */
  unsigned char enc[32];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)utf8,
                          (int)strlen(utf8));
  printf("%.*s\n", n, (char *)enc); /* Y2Fmw6k= */
  return 0;
}

이 섹션의 양 끝에는 함정이 둘 앉아 있습니다. 첫 번째는 wchar_t: 데이터가 와이드 캐릭터로 도착했다면, 인코딩 전에 바이트 시퀀스로 먼저 변환해야 합니다(Linux라면 UTF-8으로, wcstombs()나 로케일 메커니즘을 통해). wchar_t 배열의 Base64는 텍스트의 인코딩이 아니라 내부 표현의 인코딩이며, 플랫폼마다 다릅니다. 두 번째는 소스 인코딩: C 파일의 문자열 리터럴은 소스 파일 인코딩(모든 현대 프로젝트에서 UTF-8)으로 인코딩되므로, "café"를 직접 써도 파일이 정말로 UTF-8이고 컴파일러가 그렇게 알고 있다면(현대 도구체인에서는 기본으로 알고 있습니다) 동작합니다. 보내려는 바이트를 인코딩하고, 그 바이트가 무슨 뜻인지는 수신자에게 맡기세요.

이미지: 버퍼에서 문자열까지

웹 C에서 가장 흔한 "진짜" 인코딩 일: 바이너리 파일 - JPEG, PNG, 아이콘 - 이 텍스트 채널을 타야 하므로, Base64 문자열이 됩니다. 레시피는 파일을 버퍼로 읽고, 공식으로 출력 크기를 잡고, 인코딩하고, 계속 가는 것입니다. 파일 읽기 절반은 세심함이 필요합니다: C 프로그램이 실제로 깨지는 곳이 거기이니까요:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("photo.png", "rb");
  if (f == NULL) {
    return 1;
  }
  fseek(f, 0, SEEK_END);
  long size = ftell(f);
  fseek(f, 0, SEEK_SET);
  unsigned char *data = malloc((size_t)size);
  size_t got = fread(data, 1, (size_t)size, f);
  fclose(f);
  size_t out_cap = ((got + 2) / 3) * 4 + 1;
  unsigned char *enc = malloc(out_cap);
  int n = EVP_EncodeBlock(enc, data, (int)got);
  printf("png of %zu bytes becomes %d base64 chars\n", got, n);
  free(data);
  free(enc);
  return 0;
}

노트: 바이너리 읽기는 rb - 어떤 플랫폼에서든 협상 불가입니다. 텍스트 모드는 바이트를 번역해 got를 바꿀 수 있으니까요. 크기 잡기는 fseek/ftell 쌍을 씁니다(파이프와 소켓처럼 seek가 없다면, 대신 자라는 버퍼로 읽으면 됩니다). 인코딩에는 size가 아니라 got를 씁니다. 짧은 읽기가 실제로 가능한 일이기 때문이죠. 1 MB 이미지는 약 1.33 MB의 텍스트가 됩니다 - 이것이 세금이며, 미리 청구되는 것이므로, JSON 안에 base64로 넣는 이미지 페이로드는 멈춰서 진짜 파일 업로드가 더 쌌을지 물어볼 이유가 됩니다.

파일과 .b64 습관

이미지 작업의 반대쪽 얼굴: 파일의 Base64 형태를 디스크에 써야 하는 일 - .b64 사이드카, 텍스트-안전 저장소에 두는 바이너리 백업, 메일러를 위한 첨부 파일. 같은 산술, 다른 라이터. 받아들일 가치가 있는 습관은, 수신 쪽이 기대하는 길이로 명시적인 줄바꿈을 단 텍스트 출력을 쓰는 것입니다 - 이메일은 76, PEM 스타일 수신자는 64, 수신자가 자기 코드라면 아예 없어도 됩니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("data.bin", "rb");
  if (f == NULL) {
    return 1;
  }
  fseek(f, 0, SEEK_END);
  long size = ftell(f);
  fseek(f, 0, SEEK_SET);
  unsigned char *data = malloc((size_t)size);
  size_t got = fread(data, 1, (size_t)size, f);
  fclose(f);
  unsigned char *enc = malloc(((got + 2) / 3) * 4 + 1);
  int n = EVP_EncodeBlock(enc, data, (int)got);
  FILE *out = fopen("data.b64", "w");
  for (int i = 0; i < n; i += 76) {
    int chunk = i + 76 < n ? i + 76 : n;
    fwrite(enc + i, 1, (size_t)(chunk - i), out);
    fputc('\n', out);
  }
  fclose(out);
  free(data);
  free(enc);
  return 0;
}

루프는 76문자 줄과 마지막에 짧은 한 줄을 씁니다. 공백을 건너뛰는 디코더(진지한 것은 전부 그렇습니다)는 줄 길이에 전혀 신경 쓰지 않으므로, 자문의 대상은 당신의 취향이 아니라 수신자입니다. 플랫폼 줄바꿈을 원하면 쓰는 쪽에서 텍스트 모드(w)를, 수신자가 문자를 정확히 셀 거라면 wb를 유지하세요 - 셀 거라면, 당신이 약속한 것을 정확히 원합니다: 76문자에 줄바꿈 하나, 그것뿐. 바로 그 약속, 바이트가 아니라, .b64 파일을 포맷으로 만드는 것입니다.

Data URI: 진짜로 임베딩하기

Data URI (RFC 2397)는 디코딩 기사가 좋아하던 도착자의 반대쪽입니다: data:image/png;base64,...를 받는 대신, 당신이 하나를 짓습니다. 모양은 data:, 미디어 타입, ;base64, 쉼표, 페이로드 - 그리고 C에서 이를 짓는 것은 인코딩 뒤의 snprintf 하나입니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  /* 8바이트 PNG 시그니처 */
  const unsigned char png_sig[8] =
    { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
  unsigned char enc[32];
  int n = EVP_EncodeBlock(enc, png_sig, 8);
  char uri[96];
  snprintf(uri, sizeof(uri), "data:image/png;base64,%.*s",
      n, (char *)enc);
  printf("%s\n", uri);
  return 0;
}

설계 노트 세 가지. URI에 넣는 미디어 타입은 당신이 책임져야 할 주장입니다 - 진짜 파일의 매직 바이트를 먼저 스니핑하지 않으면, JPEG를 실은 data:image/png는 모든 수신자를 각기 다른 방식으로 당황하게 합니다. ;base64 플래그는 페이로드가 Base64라면 필수입니다. 빼면 페이로드는 percent-인코딩된 텍스트여야 하고, 그것은 완전히 다른 포맷입니다. 그리고 RFC의 자체 가이드라인은 data URI가 짧은 값을 위한 것이라는데: HTML 페이지 안에 5 MB 로고를 인라인으로 임베딩하는 것은 동작하지만, 진짜 자산 URL로 해결할 수 있는 설계 냄새입니다. 같은 구성은 클라이언트가 폼 데이터와 같은 요청 안에 아바타를 원할 때 JSON API에서 끊임없이 등장합니다 - 인코딩하고, 앞에 붙이고, 보내면 됩니다.

HTTP와 JSON: 살아남는 페이로드

C에서 인코딩해야 하는 가장 큰 현대적 이유는 JSON입니다. JSON 문자열은 이스케이프 규칙이 있는 문자 시퀀스이고, 원본 바이트는 그곳에 들어맞지 않습니다: 문자열 리터럴 한가운데의 NUL은 C의 문제고, JSON 문자열 안의 리터럴 줄바꿈은 무효한 JSON이며, 임의의 바이트에는 정의된 이스케이프 이야기가 필요합니다. Base64는 JSON이 결코 이스케이프할 필요가 없는 문자만 만들어 냄으로써 이 문제를 통째로 우회합니다 - 64개 알파벳 문자에, 표준 변형에선 =까지, 이 가운데 따옴표도, 백슬래시도 없습니다. 바이너리는 문자열로 들어가, 반대쪽에서 들어간 그대로 나옵니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  unsigned char enc[64];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)"hello", 5);
  char json[160];
  snprintf(json, sizeof(json),
      "{\"avatar\": \"%.*s\"}", n, (char *)enc);
  printf("%s\n", json);
  return 0;
}

이것은 {"avatar": "aGVsbG8="}를 출력합니다 - 완전하고 유효한 JSON 오브젝트, 이스케이프 기계 개입 없이, %.*s 정밀도는 NUL 종료하지 않는 인코더로 바꿀 때도 길이를 정확히 지켜 줍니다. 정직한 대가는 크기입니다: JSON 문자열로 보내는 바이트마다 4/3바이트의 대가에 필드 이름과 따옴표를 더 치러야 하므로, 10 KB 바이너리는 JSON 안에서 13.3 KB 문자열이 됩니다. 간혹 작은 블롭(아이콘, 섬네일, 서명, 토큰)이라면 좋은 대가입니다. 500 MB 업로드라면 후회할 아키텍처이며, 그 일의 도구는 진짜 파일 업로드입니다. 한 줄 더: 표준 알파벳의 +와 /는 JSON 문자열 안에서는 안전하지만, 같은 문자열이 나중에 URL 쿼리를 타면 안전하지 않습니다 - 그것은 URL-safe 섹션의 일입니다.

JWT: 세 부분, 하나의 알파벳

C에서 base64url의 플래그십 수신자는 JSON Web Token입니다. RFC 7519에 따른 컴팩트 JWT는 마침표로 이어진 base64url 인코딩 세 부분 - 헤더, 페이로드, 서명 - 이며, 하나를 짓는 것은 즐거운 연습입니다. 모든 조각이 이미 가진 함수이니까요: 표준으로 인코딩하고, URL-safe로 번역하고, 서명하고, 반복. OpenSSL의 HMAC으로 지은 HS256 토큰이 여기 있습니다:

#include <stdio.h>
#include <string.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
static void to_base64url(const char *std_b64, char *out, size_t out_cap) {
  size_t i = 0;
  for (const char *p = std_b64; *p && *p != '='; p++) {
    char c = *p;
    if (c == '+') c = '-';
    if (c == '/') c = '_';
    out[i++] = c;
  }
  out[i] = '\0';
}
int main(void) {
  const char *secret = "my-hmac-secret-key";
  const char *header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  const char *payload = "{\"sub\":\"114365\",\"name\":\"Alice\"}";
  unsigned char hb[64], pb[64];
  EVP_EncodeBlock(hb, (const unsigned char *)header,
                  (int)strlen(header));
  EVP_EncodeBlock(pb, (const unsigned char *)payload,
                  (int)strlen(payload));
  char hu[64], pu[64];
  to_base64url((const char *)hb, hu, sizeof(hu));
  to_base64url((const char *)pb, pu, sizeof(pu));
  char signing_input[256];
  snprintf(signing_input, sizeof(signing_input), "%s.%s", hu, pu);
  unsigned char mac[EVP_MAX_MD_SIZE];
  unsigned int mac_len = 0;
  HMAC(EVP_sha256(), secret, (int)strlen(secret),
      (const unsigned char *)signing_input,
      (size_t)strlen(signing_input), mac, &mac_len);
  unsigned char mb[64];
  EVP_EncodeBlock(mb, mac, (int)mac_len);
  char mu[128];
  to_base64url((const char *)mb, mu, sizeof(mu));
  printf("%s.%s.%s\n", hu, pu, mu);
  return 0;
}

구조가 가르쳐 주는 것 두 가지. 첫째, 서명 입력은 마침표로 이어진 두 URL-safe 부분입니다 - 수신자가 볼 바이트 그대로 - 그래서 base64url로의 번역은 서명 뒤에가 아니라 서명 전에 일어나야 합니다. 표준 알파벳 형태를 서명하면 수신자의 검증이 실패하는데, 이것은 컴파일되고, 돌아가고, 키 불일치처럼 보이는 버그입니다. 둘째, 헤더와 페이로드는 Base64 포장지에 든 그냥 JSON입니다: 누구나 읽을 수 있고, 그것이 설계입니다. 토큰은 봉인된 봉투가 아니라 서명한 쪽지입니다 - 그래서 가로채는 사용자가 읽어도 괜찮지 않은 것은 아무것도 넣지 마세요. 그리고 "인코딩되어 있으니까"라고 해서 패스워드를 JWT 페이로드에 넣는 일은, 영원히, 절대 하지 마세요. 이 일의 Base64 부분은 작고 지루합니다: JWT 구현에 줄 수 있는 최고의 찬사죠.

HTTP Basic 인증: 토큰 만들기

가장 오래된 인증 헤더는 동시에 가장 단순한 Base64 일입니다: username:password를 표준 알파벳으로 인코딩해, Basic이라는 단어 뒤에 놓는 것. C에서 이를 짓는 것은 두 줄이며, 유일한 세심한 점은 패스워드에 콜론이 들어 있을 수 있다는 것입니다 (수신 쪽에서 자를 때 첫 번째 콜론에서 자르는 것을 잊지 마세요):

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *user = "alice";
  const char *pass = "s3cr3t";
  char creds[128];
  snprintf(creds, sizeof(creds), "%s:%s", user, pass);
  unsigned char enc[160];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)creds,
                          (int)strlen(creds));
  printf("Authorization: Basic %.*s\n", n, (char *)enc);
  return 0;
}

이것은 Authorization: Basic YWxpY2U6czNjcjN0를 출력합니다. RFC의 경고는 보내는 쪽에도 적용됩니다: 이것은 보호가 아니라 인코딩입니다. 평문 HTTP 연결 위에서 자격 증명은 선 위의 누구에게나 base64 -d 한 번 거리에 있으므로, Basic 인증은 HTTPS 전용 습관입니다. (현대적 대안 - Bearer 토큰, mTLS - 은 모두 같은 기계를 재사용합니다: 문자열을 조립하고, 인코딩하고, 헤더에 넣는 것. 프로토콜에 헤더가 있었던 때부터, Base64는 HTTP가 구조화된 데이터를 텍스트 헤더로 밀반입하는 방법이었죠.)

이메일과 PEM: 래핑이 사는 곳

이메일이 줄 래핑 존재의 이유입니다. SMTP는 줄 길이를 제한하므로, MIME은 인코딩된 줄을 76문자로, 조상이던 PEM은 64문자로 상한을 정했고, 모든 메일 시스템은 30년 동안 그 상한을 지켰습니다. C 프로그램이 이메일 본문이나 첨부 파일을 위해 Base64를 만든다면, 래핑은 선택적 화장이 아닙니다 - 래핑되지 않은 200 KB 줄 하나면 메일 인프라의 일부가 그것을 거절하거나 망가뜨리죠. OpenSSL의 스트리밍 인코더는 래핑된 출력을 공짜로 줍니다(그 64문자 전통 길이로요). 그리고 정확히 76이 필요하면, 원샷 결과를 래핑하는 것은 5줄짜리 루프입니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  unsigned char enc[64];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)"hello world", 11);
  for (int i = 0; i < n; i += 76) {
    int chunk = i + 76 < n ? i + 76 : n;
    printf("%.*s\r\n", chunk, (char *)enc + i);
  }
  return 0;
}

\r\n에 주목하세요: 이메일은 CRLF 줄끝을 원하며, 인코딩된 텍스트가 여러 MIME 부분 중 하나라면, base64 블록을 둘러싼 모든 것이 같은 규칙을 따릅니다 - 줄 길이, CRLF 줄끝, 예외 없음. PEM 파일(대부분의 키와 인증서의 포맷)은 -----BEGIN과 -----END 마커 사이 64문자 줄이라는 같은 아이디어를 쓰고, OpenSSL 도구는 키를 다시 저장할 때 그 아머를 보기를 기대합니다 - 그래서 프로그램이 PEM을 만진다면, 64에서 래핑하고 라벨을 유지하세요. 모든 다른 곳 - JSON, URL, API, 데이터베이스 - 에서는 RFC의 규칙이 적용되고, 아예 래핑하지 않습니다.

설정과 컬럼을 통해 값을 밀반입하기

조용한 사용 사례: 텍스트 포맷을 깨뜨릴 값은 깨뜨리지 않도록 Base64로 포장됩니다. 세미콜론을 둔 데이터베이스 DSN, 따옴표를 둔 패스워드, 줄바꿈을 둔 토큰 - 운영자가 한 번 인코딩하면, 설정 파일은 문제 문자를 한 번도 보지 못합니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *dsn = "pg:host=db;password=qu\"ote";
  unsigned char enc[128];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)dsn,
                          (int)strlen(dsn));
  printf("DB_DSN_B64=%.*s\n", n, (char *)enc);
  return 0;
}

이 프로그램은 .env 파일에 붙여 넣을 정확한 줄을 출력하고, 나중에 그것을 읽는 C 코드는 getenv에 디코딩 하나입니다. 정직한 주의사항 세 가지, 전부 이것이 "아닌" 것에 관한 것. 이것은 암호화가 아닙니다: 설정 파일을 읽을 수 있는 누구나 한 호출로 값을 디코딩할 수 있으므로, 시크릿을 Base64로 포장해 놓고 보호되었다고 부르는 일은 하지 마세요. 이것은 이스케이프도 아닙니다: 포맷이 구조 보존을 필요로 한다면, 진짜 인코딩(URL은 percent-인코딩, JSON은 JSON 이스케이프)이 올바른 도구이며, Base64는 그 포맷들이 표현할 수 없는 값 - 바이너리인 것들 - 을 위한 것입니다. 그리고 크기가 듭니다: 데이터베이스 TEXT 컬럼에 Base64로 저장된 값은 원본보다 약 33퍼센트 더 많은 공간을 차지합니다. 토큰에는 괜찮은 일이고, 파일 컬럼에는 진짜 숫자입니다 (그것이 바로 BLOB 컬럼의 존재 이유죠).

셸에서 인코딩

cc 호출을 위해 손을 뻗기 전에, 표준 도구 둘 다 인코딩을 하며, 빠르다는 것을 기억하세요. coreutils가 범용 기기입니다: base64는 기본 76문자 래핑으로 인코딩하고, -w가 열을 바꾸고, -w 0는 래핑을 아예 끕니다:

base64 photo.png > photo.b64
base64 -w 0 photo.png > photo-oneline.b64
cat note.txt | base64 -w 0

OpenSSL 도구는 TLS 혈통의 포장으로 같은 일입니다: openssl base64(openssl enc -base64의 친근한 별칭)는 64문자에서 래핑하고, -A가 한 줄로 전환합니다:

openssl base64 < photo.png > photo.b64
openssl base64 -A < photo.png > photo-oneline.b64

왜 래핑 차이에 신경 쓸까요? 뒷단 파서가 문자를 셀 경우, 두 도구의 기본 출력을 서로 바꿀 수 없으니까요 - 줄당 76과 줄당 64는 파일 안의 보이는 차이이며, 공백을 제거하는 파서는 신경 쓰지 않지만 줄 길이를 검증하는 파서는 절대 신경씁니다. C 프로그램이 생산자이고 쉘이 소비자일 때(또는 그 반대), 먼저 래핑에 합의하세요. BSD 풍미 시스템을 위한 변형 노트 하나: 거기서는 디코딩 플래그가 역사적으로 -D였으며, 오래된 macOS 릴리스는 아직도 그것을 기억합니다; 인코딩 쪽은 어디든 base64이며, 이 섹션이 다루는 유일한 방향이기도 합니다.

큰 것을 스트리밍으로

수 GB 파일 하나를 malloc 하나로 인코딩하는 것은, 당신이 필요로 하지 않은 메모리 문제입니다. 스트리밍 경로는 정확히 이 일을 위해 존재하며, OpenSSL의 블록 규율은 코드를 거의 자명하게 만듭니다: 파일이 주는 만큼 EVP_EncodeUpdate에 넣고, 각 불완전 48바이트 블록의 나머지는 컨텍스트에 담아 두게 하고, 각 블록의 65 출력 바이트를 목표 파일로 바로 쓰세요. 피크 메모리는 두 버퍼 - 수십 KB - 입니다: 파일이 아무리 커도 마찬가지입니다:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  if (ctx == NULL) {
    return 1;
  }
  EVP_EncodeInit(ctx);
  FILE *in = fopen("video.mp4", "rb");
  FILE *out = fopen("video.b64", "w");
  if (in == NULL || out == NULL) {
    return 1;
  }
  char inbuf[48 * 1024];            /* 48의 배수: 깔끔한 줄 */
  unsigned char outbuf[1024 * 65 + 65];  /* 48바이트 블록당 출력 바이트 65개, 여유 공간 포함 */
  size_t got;
  while ((got = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
    int outl = 0;
    EVP_EncodeUpdate(ctx, outbuf, &outl,
        (const unsigned char *)inbuf, (int)got);
    fwrite(outbuf, 1, (size_t)outl, out);
  }
  unsigned char tail[66];
  int outl = 0;
  EVP_EncodeFinal(ctx, tail, &outl);
  fwrite(tail, 1, (size_t)outl, out);
  EVP_ENCODE_CTX_free(ctx);
  fclose(in);
  fclose(out);
  return 0;
}

그 루프에서 일을 하고 있는 설계 디테일 두 개. 입력 버퍼는 48바이트의 배수이므로, 각 호출이 인코더에게 완성된 블록만 건네고, 쓰는 모든 줄이 완성된 64문자 줄입니다; 최종 호출이 진짜 꼬리를 래핑합니다. 입력이 아무 크기로나 온다면(소켓, 느린 디스크), 컨텍스트는 정렬을 똑바로 흡수해 줍니다 - 48의 배수 선택은 정확성이 아니라 출력의 예측 가능성에 관한 것입니다. 두 번째 디테일은 출력 버퍼 크기입니다: 입력 48바이트당 65바이트에 마지막 블록의 여유(66) - 산술 섹션의 래핑 공식을 청크별로 적용한 것입니다. 진도 보고는 한 줄이면 됩니다 - out에 쓴 바이트를 파일 크기에서 계산한 총량과 비교하면 되고, 인코딩은 데이터를 키우므로, 출력 파일은 입력보다 약 33퍼센트 더 크게 도착합니다: 디스크를 미리 청구하세요.

인코딩의 날카로운 모서리

모아 놓은 함정들, 전부 C 모양의:

  • 양방향의 오프 바이 원. OpenSSL은 NUL을 제외한 길이를 돌려줍니다. Mbed TLS의 크기 조회는 NUL 자국을 포함합니다. APR은 길이에 NUL을 포함합니다. 세 라이브러리, 세 가지 집계 관례. 버퍼 크기 함수를 한 번 쓰세요(산술 섹션). 그리고 호출 지점에서 머릿속으로 산술하는 것을 멈추세요.
  • NUL은 치러야 하는 바이트입니다. 이 기사의 모든 인코더는 터미네이터를 위해 출력에서 바이트 하나 더 원하고, 당신이 손으로 쓰는 버퍼 덧붙이기 역시 같습니다. 정확히 encoded_chars(n)으로 잡은 버퍼는 누군가 printf("%s")가 동작하길 원하는 순간 바이트 하나 모자랍니다.
  • 인코딩은 실패할 수 없으므로, 오버플로우를 일어나게 두면 안 됩니다. 너무 작은 버퍼를 잡아 줄 오류 코드는 없습니다 - 인코더는 기꺼이 끝 너머로 씁니다. C에서 Base64 인코딩의 실패 모델은 전적으로 당신의 것입니다: 크기를 제대로 잡지 않으면, 진단 없이 메모리를 망가뜨립니다.
  • 중복 인코딩은 고전적인 조용한 버그입니다. 이미 Base64인 값을 인코더에 한 번 더 돌리면, 데이터 대신 Base64 문자열로 디코딩되는 완벽히 유효한 Base64 문자열이 됩니다. 증상 - "디코딩은 되는데, 틀린 것으로" - 는 오후 반나절을 찾아내게 합니다. 값이 "이미 인코딩되어" 도착했다면, 그것이 원본 데이터라고 전제하기 전에 길이가 4의 배수이고 알파벳 문자만 포함하는지 확인하세요. 인코딩된 것이라면, 인코딩을 건너뛰세요.
  • URL 안의 플러스 부호. 표준 알파벳 출력을 쿼리 문자열에 넣으면, 서버가 폼을 파싱할 때쯤 +가 스페이스로 바뀐 채 도착합니다 - percent/폼 인코딩에서 +는 스페이스이니까요. URL을 타는 토큰과 ID는 URL-safe 변형을 원합니다. 마침표.
  • 파이프 반대쪽의 잘못된 텍스트 모드. 바이너리 파일을 텍스트 모드로 읽으면 (어떤 플랫폼에서는) 바이트가 번역되어 길이가 바뀔 수 있고, 잘못된 줄끝 관례로 래핑된 Base64를 쓰면 문자를 세는 수신자가 깨집니다. 바이너리 입력은 rb, 사양이 요구하는 곳에는 명시적 \r\n 또는 \n, 그리고 C 런타임이 당신의 줄끝을 조용히 결정하게 두지 마세요.
  • 크기 산술의 int 오버플로우. ((n + 2) / 3) * 4를 int 산술로 하면 약 1.5 GB를 넘는 입력에서 오버플로우되어, 작은 양의 "필요 크기"와 힙 충돌을 낳습니다. size_t(또는 uint64_t)로 산술하세요. APR의 int 기반 API에 2 GB 상한이 있고, 그걸 공학적으로 없앨 수 없는 것도 같은 이유입니다.
  • 수신자가 기대하지 않는 곳의 래핑. RFC 4648: 주변 사양이 요구하지 않는 한 라인 피드는 금지. JSON 문자열 값 안의 줄바꿈은 무효하고, URL 안에서는 다른 요청이 됩니다. 메일은 래핑하고, PEM은 래핑하고, 그 외 어느 곳에서도 하지 마세요.

짧은 체크리스트

버퍼 크기는 추측으로가 아니라 공식으로 계산하고, 코드베이스 전체에서 크기 함수 하나를 유지하세요. 버퍼가 NUL 종료이더라도 (포인터, 길이)를 함께 다닙니다: 길이가 계약이고, NUL은 편의이니까요. 채널로 변형을 고르세요: JSON과 본문은 표준, URL과 토큰은 URL-safe, 메일과 아머는 래핑, 나머지는 래핑 없음. data URI에서 MIME 타입을 주장하기 전에 매직 바이트를 검증하세요. Base64를 암호화로, percent-인코딩의 대용으로, 시크릿을 숨기는 곳으로 쓰는 일은 절대 하지 마세요 - 그것은 자물쇠가 아니라 상자입니다. 그리고 데이터가 크다면, 스트리밍하세요: 블록 기반 인코더는 정확히 그것을 위해 설계되었고, 일정 메모리가 전부이니까요.

역사: 포장이 표준화된 과정

인코더의 역사는 줄 길이의 역사입니다. 최초의 Base64는 1990년대 초의 C 프로그램이었습니다. Privacy-Enhanced Mail (RFC 1421, 1993)은 7비트 메일을 통해 바이너리를 실어야 했고, 저자들은 64문자 줄에 문자당 6비트를 골랐습니다 - 64라는 숫자는 SMTP의 줄 길이 허용 폭의 유물이며, C 코드가 테이블 조회를 한 건 한 건 수행했습니다. MIME이 웹을 위해 같은 알파벳을 표준화했을 때 (1993년 RFC 1521, 1996년 RFC 2045), 줄을 76문자로 늘렸고, 세상은 둘 다 "그" Base64 줄 길이임을 주장하는 두 습관 - 64와 76 - 을 어깨에 메게 되었습니다. 인코더도 맞추었습니다: OpenSSL의 스트리밍 경로는 64를 유지했고(PEM 혈통), coreutils 도구는 76을 골랐고(MIME 혈통), 같은 머신의 두 도구는 줄바꿈이 어디에 가야 하는지 아직 의견이 다릅니다. 표준이 입장을 정한 것은 2006년: RFC 4648은, 참조하는 사양이 명시적으로 지시하지 않는 한 구현이 라인 피드를 추가해서는 안 된다고 했고, 그래서 이 기사의 모든 라이브러리가 기본 래핑 없음 출력을 쓰며, 래핑은 이제 이메일과 아머를 위한 선택 기능입니다. 알파벳 자체, 패딩 규칙, 그리고 "쓰지 않은 패드 비트는 0이어야 한다"는 정준 규칙은 그 이전 PEM과 MIME RFC에서 왔으며, 4648은 이 가족의 정준 규칙으로 다시 선언합니다. 그리고 그 RFC 11절은 참조 구현 - ISO C99 프로그램 - 을 가리키는데, 코드 자체가 "절차상의 이유로 이 RFC에 포함할 수 없었다"며 외부에 호스팅되어 있습니다 - 이 포맷에서 C가 이등 시민이 아님을 다시 상기시켜 주는 것이죠. C 표준 라이브러리는 한 번도 따라잡지 못했습니다: C89는 이 중 무엇도 존재하기 전인 1990년에 동결되었고, 2024년의 C23도 여전히 Base64 함수 없이 납품됩니다. 그래서 링크하는 라이브러리들이 표준이며, 그들 사이의 선택은 작지만 진짜 설계 결정입니다 - 이 기사가 다뤄 온 것이 바로 그것입니다.

소소한 괴상한 사실들

그냥 재미있는 사실들, 전부 C에서 포장 쪽에 관한 것:

  • "64"는 밑수입니다: 출력 문자마다 6비트이고, 2의 6제곱은 64입니다. 포맷은 C가 정수를 이름 붙이듯, 숫자의 실제 값으로 자기 알파벳에 이름을 붙였습니다.
  • OpenSSL 스트리밍 인코더의 48바이트 블록은 임의의 집계가 아닙니다: 입력 48바이트는 정확히 3의 16그룹이고, 출력 64문자는 정확히 4의 16그룹입니다. 두 숫자 모두 16의 배수인데, 하드웨어와 캐시 라인을 행복하게 만드는 바로 그 종류의 반듯함 - 적어도 코드를 읽는 인간들을 행복하게 만들죠.
  • 입력 1바이트는 4문자로 인코딩되며, 그중 둘은 =입니다. 가장 작을 수 있는 비어 있지 않은 페이로드는 패딩 50퍼센트 - 포맷에서 가장 낭비적인 인코딩이며, 틀리기 쉬운 만큼 모든 테스트 스위트가 쓰는 것이죠.
  • 이 기사에서 조회를 상수 시간에 하는 유일한 인코더가 Mbed TLS인데, 임베디드 암호를 쓰는 사람들은 코덱이 암호가 아니라 할 때도 가변 시간 테이블 인덱싱을 신뢰하지 않습니다. 패러노이아는 전이되죠.
  • 정준 인코딩 규칙 - 쓰지 않은 패드 비트는 0이어야 한다 - 는, 이를 어기면 서로 다른 두 문자열이 같은 바이트로 디코딩될 수 있다는 사실을 알게 될 때까지 자명해 보입니다. 그것은 존재하는 모든 "이 문자열이 저 파일의 인코딩인가?" 검사를 깨뜨리죠. 당신의 인코더들은 전부 지키고 있습니다. 그래서 base64는 해시-안정적인 표현이며, 콘텐츠 스토어에서 파일 이름의 대용으로 쓸 수 있습니다.
  • OpenSSL의 EVP_EncodeBlock은 드문 C 함수 중 하나로, 반환 값, 자기 출력, NUL 터미네이터가 모두 일치합니다: n문자를 쓰고, NUL을 쓰고, n을 돌려줍니다. 오프 바이 원으로 유명한 언어에서, 그것은 평화의 순간입니다.
  • APR-Util은 EBCDIC이 무엇을 뜻하는지 묻는 여기의 유일한 인코더입니다: Apache가 아직 글자 순서가 ASCII와 다른 머신에서 돌거든요. 그 머신들에서는 문자열을 "인코딩"하는 일이 먼저 알파벳을 조용히 재정렬하는 것을 포함합니다.
  • 빈 입력은 모든 라이브러리에서 패딩도 줄바꿈도 없이 빈 문자열로 인코딩됩니다. 포맷의 항등원으로, 넷 모두에서 제자리를 지키고, 그래서 이보다 싼 유닛 테스트는 없습니다.

디코더 쪽으로 뒤집기

그래서 이것이 포장 쪽입니다: 산술, 인코더 넷, 변형들, 그리고 바이트가 가르는 곳들. 이는 일의 잔잔한 반편입니다: 인코딩에는 무효한 입력도, 당신과 다른 의견을 내는 디코더도 없으니까요. 반대 방향 - 바깥 세계의 Base64를 만나고 바이트를 되찾는 일 - 이 고통이 집중되는 곳입니다: 제로 패딩된 꼬리, 조용한 절단, 엄격 대 관대 알파벳, 그리고 꼬리 줄바꿈을 먹는 명령줄. C에서의 Base64 디코딩은 이 페이지에서 링크된 관련 기사에서 심층으로 다루며, 이 기사와 자연스러운 짝입니다: 인코더가 상자를 쓰고, 디코더가 그것을 여는데, 둘 사이에는 C 프로그램이 겪는 모든 Base64 일이 있습니다.

마지막 업데이트: 2026-09-08

관련 문서: C에서의 Base64 디코딩: 완전한 가이드