파일 업로드
  1. base64hub
  2. 코드로 다루는 Base64
  3. C
코드

C에서 Base64 사용하기

C 표준 라이브러리에는 Base64 함수가 없어서 대부분의 프로젝트가 직접 구현을 들고 다닙니다. Python의 base64 모듈과 대조해 퍼징한 짧은 구현과, 흔히 잘못되는 세부 사항을 소개합니다.

인코더

입력 3바이트마다 각각 6비트를 담는 4문자가 됩니다. 1바이트나 2바이트로 된 마지막 그룹은 =로 패딩합니다. 비트 배치는 Base64의 작동 원리 페이지에서 차근차근 설명합니다.

#include <stddef.h>
#include <stdint.h>

static const char B64[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/* Output size for n input bytes, without the terminating NUL. */
size_t b64_encoded_len(size_t n) { return 4 * ((n + 2) / 3); }

/* Writes b64_encoded_len(n) chars plus a NUL to out. */
void b64_encode(const uint8_t *in, size_t n, char *out) {
    size_t i = 0;
    for (; i + 2 < n; i += 3) {
        uint32_t v = (uint32_t)in[i] << 16 | (uint32_t)in[i + 1] << 8 | in[i + 2];
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = B64[v >> 6 & 63];
        *out++ = B64[v & 63];
    }
    if (i < n) {                         /* 1 or 2 bytes left */
        uint32_t v = (uint32_t)in[i] << 16;
        if (i + 1 < n) v |= (uint32_t)in[i + 1] << 8;
        *out++ = B64[v >> 18 & 63];
        *out++ = B64[v >> 12 & 63];
        *out++ = (i + 1 < n) ? B64[v >> 6 & 63] : '=';
        *out++ = '=';
    }
    *out = '\0';
}

i < n - 2 대신 i + 2 < n으로 쓴 반복 조건이 중요합니다. size_t에서는 n이 0이나 1일 때 n - 2가 아주 큰 수로 랩어라운드됩니다. uint32_t로의 캐스트는 in[i] << 16이 부호 있는 int로 계산되는 것을 막습니다.

엄격한 디코더

이 디코더는 정규 형식의 패딩된 Base64가 아닌 것은 모두 거부합니다. 잘못된 길이, 알파벳에 없는 문자, 중간에 있는 패딩, 0이 아닌 패딩 비트가 해당합니다.

static int b64_val(unsigned char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '+') return 62;
    if (c == '/') return 63;
    return -1;
}

/* Returns the number of bytes written, or -1 on invalid input.
   out needs room for 3 * (len / 4) bytes. */
long b64_decode(const char *in, size_t len, uint8_t *out) {
    if (len % 4 != 0) return -1;
    size_t o = 0;
    for (size_t i = 0; i < len; i += 4) {
        int a = b64_val(in[i]), b = b64_val(in[i + 1]);
        int c = b64_val(in[i + 2]), d = b64_val(in[i + 3]);
        int last = (i + 4 == len);
        if (a < 0 || b < 0) return -1;
        if (c < 0) {                     /* must be "xx==" at the end */
            if (!last || in[i + 2] != '=' || in[i + 3] != '=' || (b & 15)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
        } else if (d < 0) {              /* must be "xxx=" at the end */
            if (!last || in[i + 3] != '=' || (c & 3)) return -1;
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
        } else {
            out[o++] = (uint8_t)(a << 2 | b >> 4);
            out[o++] = (uint8_t)(b << 4 | c >> 2);
            out[o++] = (uint8_t)(c << 6 | d);
        }
    }
    return (long)o;
}

b64_val이 unsigned char를 받는 이유는 char가 부호 있는 타입인 플랫폼에서 127보다 큰 바이트가 음수 인덱스가 되지 않게 하기 위해서입니다. 항목 256개짜리 조회 테이블이 더 빠르지만, if 연쇄가 눈으로 검토하기에는 더 쉽습니다.

사용법과 결과

#include <stdio.h>
#include <string.h>

int main(void) {
    const char *s = "héllo wörld";          /* source file saved as UTF-8 */
    char enc[64];
    b64_encode((const uint8_t *)s, strlen(s), enc);
    printf("%s\n", enc);                     /* aMOpbGxvIHfDtnJsZA== */

    uint8_t dec[64];
    long n = b64_decode(enc, strlen(enc), dec);
    printf("%ld %.*s\n", n, (int)n, dec);    /* 13 héllo wörld */
    return 0;
}

C에는 텍스트 인코딩이라는 개념이 없습니다. 문자열 리터럴의 바이트는 소스 파일에 들어 있는 그대로입니다. 소스 파일이 UTF-8이고 GCC의 기본 실행 문자 집합을 쓰면 UTF-8이 됩니다.

몇 가지 대표적인 입력에 대해 b64_decode가 반환하는 값:

입력반환값이유
""0빈 입력은 올바름
aGk=2hi
aGk-1길이가 4의 배수가 아님
aGk=!!-1길이와 문자
aGl=-10이 아닌 패딩 비트
PDw_Pz8-Pg==-1URL-safe 문자
a===-1패딩이 너무 많음

gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined로 컴파일한 뒤, 무작위 버퍼 2,000개를 왕복 변환하고, 무작위 문자열 20,000개에 대한 허용/거부 판정을 Python의 b64decode(validate=True)와 재인코딩 검사 결과와 비교했습니다. 차이도, 새니타이저 보고도 없었습니다. 직접 가진 문자열을 시험하려면 같은 규칙을 적용하는 Base64 검사기를 사용하세요.

파일이나 stdin을 조각으로 인코딩하기

크기가 3의 배수인 블록으로 읽으면, 이어 붙인 출력이 파일 전체를 한 번에 인코딩한 결과와 같아집니다.

int main(void) {
    uint8_t in[3 * 1024];                /* multiple of 3: no padding mid-stream */
    char out[4 * 1024 + 1];
    size_t n;
    while ((n = fread(in, 1, sizeof in, stdin)) > 0) {
        b64_encode(in, n, out);
        fputs(out, stdout);
    }
    return ferror(stdin) ? 1 : 0;
}

1,000,003바이트짜리 무작위 파일에서 ./b64 < big.bin은 base64 -w 0 big.bin과 같은 출력을 냈습니다. fread는 버퍼가 가득 차거나 입력이 끝날 때까지 계속 읽으므로, 파이프에서 짧게 읽히더라도 중간에 패딩이 끼어들지 않습니다. Windows에서는 먼저 _setmode(_fileno(stdin), _O_BINARY)를 호출하세요. 그렇지 않으면 CRLF 변환 때문에 바이너리 입력이 손상됩니다.

URL-safe 출력

인코딩한 뒤 변환합니다. 두 문자를 바꾸고 패딩을 제거하면 됩니다.

void b64_to_url(char *s) {
    char *w = s;
    for (char *r = s; *r; r++) {
        if (*r == '=') break;
        *w++ = (*r == '+') ? '-' : (*r == '/') ? '_' : *r;
    }
    *w = '\0';
}
/* bytes {0xfb, 0xff}: "+/8=" -> "-_8" */

디코딩할 때는 b64_val에서 -를 62에, _를 63에 대응시키고, = 없이 2문자나 3문자로 된 마지막 그룹을 받아들이도록 하세요. Base64와 Base64URL 비교와 패딩 설명을 참고하세요.

직접 작성한 Base64의 흔한 버그

  • 버퍼 크기 1 차이 오류. NUL 종료 문자를 빠뜨리거나, 출력 크기를 n * 4 / 3으로 잡는 경우입니다. 후자는 n이 3의 배수가 아닐 때마다 너무 작습니다. 크기 계산기와 크기 증가 가이드에서 정확한 수치를 확인하세요.
  • 부호 있는 char로 조회. c가 음수이면 table[c]가 배열 밖을 읽습니다.
  • 바이너리 데이터에 strlen 사용. 디코딩한 출력에는 0x00이 들어 있을 수 있습니다. strlen이 아니라 반환된 길이를 사용하세요.
  • 트라이그래프. 엄격한 C99/C11 모드에서는 "<<???>>" 같은 테스트 문자열에 트라이그래프 ??>가 들어 있어 컴파일러가 이를 }로 바꿉니다. GCC는 -Wtrigraphs로 경고합니다. "<<?\?\?>>"처럼 이스케이프하거나 바이트 배열을 사용하세요. C23에서는 트라이그래프가 제거되었습니다.

std::string과 std::vector를 쓰는 C++ 버전은 C++에서 Base64 사용하기에 있습니다.

자주 묻는 질문

출력 버퍼는 얼마나 커야 하나요?

인코딩에는 4 * ((n + 2) / 3)자에 종료 NUL용 1바이트를 더한 크기가 필요합니다. 디코딩에는 3 * (len / 4)바이트면 항상 충분하며, 실제 길이는 패딩 때문에 최대 2바이트 짧습니다.

대신 쓸 수 있는 Base64 함수가 OpenSSL이나 glibc에 있나요?

OpenSSL에는 EVP_EncodeBlock과 EVP_DecodeBlock이 있고, BIO 계층으로 Base64를 스트리밍할 수도 있습니다. glibc에는 공개 Base64 API가 없습니다. 프로젝트가 이미 이런 라이브러리를 링크하고 있다면 쓰는 것이 합리적이고, 그렇지 않다면 새 의존성을 추가하는 것보다 아래 코드가 더 간단합니다.

제 디코더는 왜 aGl= 같은 문자열을 받아들이나요?

aGl=와 aGk=는 둘 다 hi로 디코딩되지만, 앞의 것은 패딩 비트가 0이 아닙니다. 관대한 디코더는 이 비트를 무시합니다. 이 페이지의 디코더는 마지막 그룹에서 (c & 3)과 (b & 15)를 검사해 이를 거부합니다.

계속 읽기