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

C++ (Cpp)에서의 Base64 인코딩: 완전한 가이드

역방향 문제는 헤드라인이 더 큰 쪽입니다: 당신에게는 바이트가 있습니다 - 인증서, 이미지, 랜덤 덩어리, 서명 - 그리고 그것들은 텍스트밖에 모르는 어딘가를 여행해야 합니다: JSON 필드, 이메일 헤더, URL, 환경 변수. 이 사이트의 홈 페이지는 포맷을 깊이 파고들므로, 여기서는 줄인 버전만 짚습니다: 바이트 3개가 알파벳 문자 4개가 되고, 짧은 꼬리는 = 표시 하나나 둘을 받고, 인코딩된 형태는 원본보다 약 33퍼센트 더 큽니다. 인코딩은 커지는 방향이므로, 이 글의 모든 버퍼는 그에 맞춰 크기를 잡았고, 계산은 한 줄 - 4 * ((n + 2) / 3) - 입니다. 어느 인코더를 골라도 변하지 않죠.

디코딩 쪽과 마찬가지로, C++ 자체는 단 하나의 바이트도 당신을 대신해 인코딩해 주지 않습니다. 표준 라이브러리는 base64 함수를 기를 시간을 30년이나 가졌는데 그 시간을 온전히 다른 데 썼으므로, 모든 C++ 프로그램은 성격이 아주 다른 인코더 네 개의 벤치에서 하나를 데려오거나, 40줄가량을 직접 쓰는 옵션을 택합니다. 한 마리는 1990년대부터 TLS를 질주해 온 일꾼으로, 허락도 구하지 않고 패딩을 하고, NUL로 끝내며, 줄을 감습니다. 한 마리는 저자들이 "detail"이라고 표기한 네임스페이스에 숨어 있는 빠른 헤더 전용 코덱입니다. 한 마리는 2002년 반복자로, 아마 패드 문자를 한 번도 만난 적이 없는 듯합니다. 또 한 마리는 운영체제가 수십 년째 싣고 있는 함수로, 당신의 토큰 끝에 CRLF를 붙여 줍니다. 그리고 다섯 번째 옵션은 당신의 것입니다. 각 인코더가 무엇을 더하고, 무엇을 거부하고, 조용히 무엇을 붙이는지 알게 되면, 인코딩은 더 이상 오프바이원 버그의 공급원이 아닙니다. 자, 싸기 시작해 볼까요.

표준은 패커를 한 번도 싣지 않았다

C++98 이후의 모든 표준 - C++26까지 여덟 개나 됩니다 - 은 64문자 알파벳을 쳐다보고는 다음으로 넘어갔습니다. <base64>도 없고, std::base64도 없고, 당신의 바이트를 싸 줄 <string>이나 <vector> 안의 것도 없습니다. C++26의 기술적 작업은 2026년 3월 영국 Croydon의 ISO C++ 회의에서 완성되어 투표로 통과했고(114-12-3), 텍스트 코덱 업무를 위한 <text_encoding> 헤더를 실제로 추가합니다. 위원회의 다음 회의들, 2026년 6월 Brno와 2026년 11월 브라질 Búzios에서는 C++26을 되돌아보는 것이 아니라 C++29 워크 드래프트를 엽니다. Base64는 표준에 없으며, 위원회를 탓하기는 어렵습니다: 텍스트 인코딩은 문자셋에 대한 것이지, base64는 바이트에 대한 것이므로, 새 헤더는 애초에 맞는 집이 아니었죠. 실무에서는 생태계가 일을 해 왔습니다. OpenSSL의 EVP base64 루틴은 OpenSSL 릴리스마다 들어 있고, Boost 라이브러리는 독립적인 인코더 둘을 싣고, Windows는 그 일을 위한 플래그 테이블과 함께 CryptoAPI 함수를 싣고, 40줄 스니펫은 2008년부터 이 언어를 건너 복사 붙여넣기를 해 왔습니다. 프로젝트가 CMake 기반이라면, 전체 의존성 설정은 3줄입니다:

find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)

기억할 Boost 릴리스는 2026년 8월의 1.92.0입니다. 1998년에 설립되어 1999년 첫 릴리스부터 라이브러리를 싣고 온 프로젝트의 것이죠. 아래 나올 Boost 인코더 둘 다 헤더 전용입니다 - 링크할 것이 전혀 없으며 - OpenSSL은 -lcrypto를 원하는데, TLS를 건드리는 대부분의 C++ 프로그램은 이미 바이너리 안에 그것을 가지고 있습니다.

먼저, 수학: 이 글의 모든 버퍼 크기를 잡다

Base64는 바이트를 3개씩 묶으므로, 출력 길이의 모양은 알면 더 이상 놀라울 것이 없습니다: 입력 3바이트마다 4문자가 나가고, 짧은 꼬리는 풀 그룹까지 패딩됩니다. 입력 n바이트에 대한 정확한 개수는:

4 * ((n + 2) / 3)

+2가 올림 함수 트릭입니다: 정수 나눗셈은 아래로 버트리므로, 2를 먼저 더하면 3의 다음 배수로 위로 반올림됩니다. 거기서부터, 이 글의 모든 버퍼 크기는 대입입니다. OpenSSL 원샷 함수는 인코딩된 데이터와 끝에 붙이는 NUL을 담아 버퍼를 원합니다 - man 페이지는 입력 16바이트가 24 인코딩 바이트에 NUL 1개를 더해 전부 25바이트가 되고, 함수는 NUL을 뺀 길이를 돌려준다는 예로 계약을 보여 줍니다. 그 스트리밍 경로는 입력을 48바이트 블록으로 처리하며, man 페이지는 블록당 출력 65바이트(64문자 + 매 블록이 언제나 만들어내는 줄바꿈)에 NUL 1바이트 더로 크기를 잡습니다. Boost.Beast 헤더는 정확한 공식을 constexpr 함수로 건네 줍니다. 그리고 당신의 코드는 (n + 2) / 3 * 4를 예약하고 끝냅니다. 실제로 만나게 될 숫자들:

입력 출력 (패딩) 볼 것
1바이트 4문자 가장 작은 패딩된 형태: QQ==
2바이트 4문자 데이터 3문자와 패드 1개
3바이트 4문자 풀 그룹 하나, 패딩 없음
48바이트 64문자 OpenSSL 스트리밍 블록 정확히 하나
500바이트 668문자 64에서 줄바꿈하면 11줄, 줄바꿈 포함 679문자
1 GB 약 1.33 GB 세금만큼 컬럼, 파일, 배선을 버짓하세요

받는 쪽이 고정 크기 컬럼, 버퍼, 텍스트 파일의 한 줄이라면, 이 공식이 곧 전체 설계 문서입니다. 당신이 물릴 수 있는 방향은 반대 방향입니다: 디코딩 쪽은 3n/4에서 패드를 뺀 것이 필요하고, 인코딩 공식으로 크기를 잡은 디코딩 버퍼는 메모리 버그 티켓으로 자라나는 클래식한 과할당입니다. 수축 방향의 크기를 잡는 것은 자매 가이드의 문제입니다. 여기서는 오로지 자라기만 하죠.

전반상이 이렇습니다. 차이는 모든 행이 동일하게 구현하는 핵심 파킹에 있는 것이 아니라, 그 부가물 - 패딩, 줄바꿈, NUL - 에 있습니다:

인코더 출처 패딩 버짓할 추가 바이트 꼭 기억할 특이점
EVP_EncodeBlock <openssl/evp.h>, 링크 -lcrypto 항상 1 (버퍼 안의 NUL) man 페이지의 16바이트 예가 계약
EVP_EncodeUpdate + Final 동일 항상 48바이트 블록당 65 64문자에서 하드 줄바꿈, 매 블록이 줄바꿈으로 끝남
Boost.Beast encode boost/beast/core/detail/base64.hpp, 헤더 전용 항상 0 detail라는 이름의 네임스페이스에 거주
Boost.Serialization 반복자 boost/archive/iterators/base64_from_binary.hpp, 헤더 전용 절대 안 함 0 - 패드 1~2개는 당신이 직접 더함 도구상자에서 가장 오래된 인코더, 2002
CryptBinaryToStringA wincrypt.h, crypt32.lib 항상 2 (CRLF), NOCRLF가 아니라면 나머지 도구상자가 없는 URL 안전 플래그를 가짐
당신의 40줄 어디에도 없다: 네 것이니까 네 선택 네 선택 모든 엣지 케이스를 영원히 네가 책임진다

핵심 알고리즘은 모든 행에서 동일합니다 - 1987년 포맷의 위로할 부분이 바로 그것. 다른 것은 각 구현이 페이로드 주위에 무엇을 더하느냐이고, 이 글의 함정 대부분은 그 부가물 하나가 그것을 기대하지 않은 소비자한테 부딪히는 것입니다.

OpenSSL: 당신의 TLS 스택이 이미 링크한 인코더

프로그램이 이미 TLS를 위해 OpenSSL을 링크하고 있다면, 추가할 것이 아무것도 없습니다. 원샷 함수는 한 번의 호출입니다:

int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n);

소스 바이트와 길이를 주면, 패딩된 한 줄 인코딩을 씁니다. 이 계약은 외울 가치가 있습니다. man 페이지가 예로 명시하니까요: 입력 3바이트마다 출력 4바이트; 3으로 나누어 떨어지지 않는 꼬리는 패딩되어 출력이 언제나 4로 나누어 떨어지도록 하고; 그 위에 NUL 종료 문자가 추가됩니다. 문서화된 예는 16바이트가 들어가 24 인코딩 바이트에 NUL 1개를 더해 버퍼에 전부 25바이트이고, 함수는 24를 돌려줍니다 - NUL을 뺀 길이죠. 버퍼를 그에 맞게 잡으면, 래퍼는 몇 줄이면 됩니다:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>

std::string openssl_encode(const std::string &in) {
  std::string out;
  out.resize(4 * ((in.size() + 2) / 3) + 1);
  int n = EVP_EncodeBlock(reinterpret_cast<unsigned char *>(out.data()),
                          reinterpret_cast<const unsigned char *>(in.data()),
                          static_cast<int>(in.size()));
  if (n < 0) return {};
  out.resize(static_cast<size_t>(n));
  return out;
}

int main() {
  std::printf("%s\n", openssl_encode("Mane").c_str());
  std::printf("%s\n", openssl_encode("M").c_str());
  std::printf("%s\n", openssl_encode("").c_str());
}

std::string이 C였다면 당신에게 강제했을 일을 대신해 주는 것에 주목하세요: 반환된 길이로 정확히 자라므로, OpenSSL이 붙인 NUL은 추적 길이 너머에 그냥 놓여 있어 페이로드가 될 일이 결코 없습니다. "Mane"를 인코딩하면 TWFuZQ==, 패드 하나를 가진 클래식한 4문자 꼬리가 나옵니다; 1바이트를 인코딩하면 2문자 패드 분장을 한 2문자 데이터 쌍이 나옵니다; 아무것도 인코딩하면 빈 문자열이 나옵니다 - base64 인코더가 정확히 항등 함수처럼 행동하는 유일한 경우요. 함수 전체에서 진짜 로직 한 줄은 resize입니다: "쓴 바이트 수 + NUL"을 "페이로드 그 자체"로 바꿔 주죠.

조각조각 도착하는 데이터 - 파일, 소켓, 버퍼링하고 싶지 않은 스트림 - 에는 OpenSSL이 먹이고 마무리하는 컨텍스트를 줍니다. man 페이지의 블록 계산은 이례적으로 명확합니다. 48바이트 풀 블록만 즉시 처리되고, 나머지는 컨텍스트 안에 머물다가 다음 호출이나 마지막 호출로 빠져 나옵니다. 처리된 각 블록은 64문자 + 줄바꿈 - 65바이트 - 을 쓰고, 마지막 호출이 부분 블록을 다룹니다. 그래서 문서화된 상한은 65바이트 + NUL이죠. 호출 전에 알아 둘 결과: 이 API는 64문자에서 줄바꿈합니다. 설정할 수 없습니다. 스트리밍 인코더란 바로 그런 것입니다.

#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>

std::string openssl_encode_wrapped(const std::string &in) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_EncodeInit(ctx);
  std::string out;
  out.reserve(4 * ((in.size() + 2) / 3) + in.size() / 48 + 2);
  std::vector<unsigned char> buf(128);
  int outl = 0;
  for (size_t pos = 0; pos < in.size();) {
    size_t take = std::min<size_t>(48, in.size() - pos);
    EVP_EncodeUpdate(ctx, buf.data(), &outl,
                     reinterpret_cast<const unsigned char *>(in.data()) + pos,
                     static_cast<int>(take));
    out.append(reinterpret_cast<const char *>(buf.data()), outl);
    pos += take;
  }
  EVP_EncodeFinal(ctx, buf.data(), &outl);
  out.append(reinterpret_cast<const char *>(buf.data()), outl);
  EVP_ENCODE_CTX_free(ctx);
  return out;
}

int main() {
  std::string s = openssl_encode_wrapped(std::string(500, 'A'));
  std::printf("500 bytes -> %zu chars\n", s.size());
  int lines = 0;
  size_t longest = 0, run = 0;
  for (char c : s) {
    if (c == '\n') { lines++; run = 0; }
    else run++;
    longest = std::max(longest, run);
  }
  std::printf("lines=%d longest=%zu lastchar=%c\n", lines, longest, s.back());
}

글자 A 500바이트를 먹여 보면, 계산은 man 페이지가 약속한 대로 정확히 맞습니다: 인코딩 문자 668개, 그리고 출력이 64문자 줄로 자라기 때문에 11줄, 전부 679문자, 그리고 마지막 글자는 줄바꿈입니다. 그 뒤쪽 줄바꿈이 바로 소비자를 깨는 것입니다: 결과를 JSON 문자열에 붙여 놓으면 따옴표가 있어야 할 자리에 제어 문자가 있고, 토큰 세그먼트로 쓰면 새 세그먼트를 발명한 셈이죠. 경험 법칙: 한 줄 페이로드(토큰, 헤더, 설정 값)에는 블록 API, 소비자가 MIME 모양의 줄바꿈된 출력을 원하면 스트리밍 API, 그리고 확신이 없으면 페이로드가 그것을 기대하지 않는 경계를 건너기 전에 while (out.back() == '\n') 루프로 뒤쪽 줄바꿈을 벗기세요.

Boost.Beast: detail:: 네임스페이스 속 빠른 팩터

Boost의 HTTP 라이브러리는 boost/beast/core/detail/base64.hpp라는 뜻밖의 주소에 base64 코덱을 싣고 있습니다. detail:: 네임스페이스는 "이건 우리의 내무 문제"라는 Boost의 표현 방식이고, 유지보수자들은 이 코덱을 공개 API로 승격하는 것을 거절해 왔습니다. 그래도 모두가 씁니다: 작고, 빠르고, 헤더 전용이라서요(include 전에 BOOST_BEAST_HEADER_ONLY를 정의하면 링크할 것이 아무것도 없습니다). 그리고 Boost.Beast의 WebSocket 핸드셰이크가 Sec-WebSocket-Accept 계산에 쓰는 코덱이 바로 이것이라, 실 트래픽을 몇 년째 씹어 먹고 있습니다.

인코딩 쪽의 API는 거의 모욕할 정도로 담담합니다. constexpr 헬퍼가 정확한 출력 크기를 줍니다 - 4 * ((n + 2) / 3), 수학 섹션과 같은 공식, 이제는 컴파일러가 확인해 주죠 - 그리고 encode 함수는 패딩된 결과를 당신의 버퍼에 쓰고, 몇 문자를 썼는지 알려 줍니다. 오류 채널은 없습니다. 인코딩은 실패할 수 없기 때문이죠: 어떤 바이트든 유효한 입력이고, 출력 길이는 입력 길이의 순수 함수입니다. 래퍼는 이렇습니다:

#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace b64 = boost::beast::detail::base64;

std::string beast_encode(const std::string &in) {
  std::string out(b64::encoded_size(in.size()), '\0');
  std::size_t n = b64::encode(out.data(), in.data(), in.size());
  out.resize(n);
  return out;
}

int main() {
  std::printf("%s\n", beast_encode("Mane").c_str());
  std::printf("%s\n", beast_encode("M").c_str());
}

"Mane"를 인코딩하면 TWFuZQ==, 단일 바이트 M을 인코딩하면 TQ==가 나옵니다 - OpenSSL 래퍼가 만든 것과 같은 바이트, 걱정할 NUL도 없고 벗길 줄도 없습니다. 아카이브해 둘 것 두 가지. 첫째, 계보: 소스는 Vinnie Falco의 2016-2019 저작권이며, 푸터의 일부는 Rene Nyffenegger의 2004-2008 스니펫에 기원을 돌립니다 - C++ base64 이야기를 시작한 그 민요와 같은 것이, 이제는 Boost 안에, 당신의 바이너리 안에 실려, 웹 전체를 대신해 WebSocket 핸드셰이크를 하죠. 둘째, 실무적인 것: 코덱이 패딩하고 줄바꿈을 절대 하지 않으므로, 한 줄이 되어야 하는 모든 것 - 토큰, 헤더, API 페이로드 - 에 맞는 도구입니다. 그리고 encoded_size 공식은 정확히 맞는 버퍼를 줍니다. 근사가 아니라서요.

Boost.Serialization: 패딩의 존재를 잊은 반복자

C++ 생태계에서 가장 오래된 base64는 함수가 아니라, 2002년 Robert Ramey가 Boost의 직렬화 라이브러리를 위해 쓴 구성 가능한 반복자 어댑터 집합입니다. 인코딩 방향은 어댑터 둘의 체인입니다: 원시 바이트를 8에서 6으로 다시 묶는 폭 변환기, 그리고 각 다시 묶인 값을 알파벳 문자로 바꾸는 반복자:

#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace it = boost::archive::iterators;

std::string boost_iter_encode(const std::string &in) {
  using enc =
      it::base64_from_binary<it::transform_width<const char *, 6, 8>>;
  std::string out(enc(in.data()), enc(in.data() + in.size()));
  switch (in.size() % 3) {
    case 1: out += "=="; break;
    case 2: out += '=';  break;
    default: break;
  }
  return out;
}

int main() {
  std::printf("%s\n", boost_iter_encode("Mane").c_str());
  std::printf("%s\n", boost_iter_encode("M").c_str());
}

반복자는 핵심 파킹만 하고, 아무것도 더하지 않습니다 - 패딩도, NUL도, 줄바꿈도, 오류 채널도, 없습니다. 핵심 파킹은 실패할 수 없기 때문이죠. "Mane"를 인코딩하면, 반복자는 담담한 얼굴로 6문자, TWFuZQ를 건네 줍니다: 4바이트의 진짜 인코딩은 8문자이고, 2002년 반복자가 그것에 관심을 가질 리는 없었으니까요. 그래서 switch 문장이 핵심이지, 장식이 아닙니다: 그룹에서 1바이트 모자라면 패드 두 개, 2바이트 모자라면 하나. switch를 빼면 같은 체인이 됩니다. 바로 그 단계를 잊었을 때 당신이 얻게 되는 것이요. 결과는 관대한 디코더 아래에서는 잘 디코딩되고, 엄격한 디코더에서는 실패하며, API 소비자의 오류 메시지를 미스터리로 만들어 버리는 문자열입니다. (같은 반복자 집합의 디코딩 쪽이 바로 흘러간 공백 하나에 예외를 던지는 쪽입니다 - 자매 가이드에서 더 자세히.)

40줄, 의존성 제로

Base64는 작아서, 올바른 인코더 하나를 직접 소유하는 것은 충분히 그럴듯한 일입니다. 그리고 C++에서는 그 대가가 어떤 언어보다 좋습니다: std::string이 버퍼 관리를 편하게 해 주고, 공식이 앞쪽에서 정확한 크기를 주고, 손수 만든 인코더는 의견이 전혀 없는 쪽 - NUL도 없고, 줄바꿈도 없고, 플랫폼 습관도 없고 - 설정 파일 아래나 API 경계에서 원하는 것이 바로 그것입니다. 이 버전은 64문자 테이블에 대해 3바이트 그룹으로 쌉니다:

#include <cstddef>
#include <cstdio>
#include <string>

std::string base64_encode(const std::string &in) {
  static const char *table =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  std::string out;
  out.reserve((in.size() + 2) / 3 * 4);
  const unsigned char *p =
      reinterpret_cast<const unsigned char *>(in.data());
  size_t n = in.size();
  for (size_t i = 0; i < n; i += 3) {
    unsigned v = p[i] << 16;
    if (i + 1 < n) v |= p[i + 1] << 8;
    if (i + 2 < n) v |= p[i + 2];
    out.push_back(table[(v >> 18) & 63]);
    out.push_back(table[(v >> 12) & 63]);
    out.push_back(i + 1 < n ? table[(v >> 6) & 63] : '=');
    out.push_back(i + 2 < n ? table[v & 63] : '=');
  }
  return out;
}

int main() {
  std::printf("%s\n", base64_encode("Mane").c_str());
  std::printf("%s\n", base64_encode("M").c_str());
  std::printf("%s\n", base64_encode("M\312\277").c_str());
}

부품들을 걸어 지나가 보겠습니다. reserve 행이 수학 섹션입니다: (n + 2) / 3 * 4문자, 정확히, 그래서 루프 한가운데서 재할당이 일어나지 않습니다. const unsigned char *로의 reinterpret_cast는 의식이 아닙니다 - char가 signed인 플랫폼에서, 127을 넘는 바이트는 그렇지 않으면 음수이고, 그것이 테이블 인덱스에 닿는 순간 실험복을 입은 미정의 동작이 됩니다. 각 반복은 최대 3바이트를 24비트 값으로 끌어들이고, 6비트 조각 넷을 테이블에 밀어 넣고, 꼬리에서는 없는 바이트 자리에 =를 내뱉습니다 - i + 1 < n과 i + 2 < n 가드가 바로 전체 패딩 로직이죠. "Mane"를 넣으면 TWFuZQ==가 나옵니다. M 하나를 넣으면 TQ==가 나옵니다. 127을 넘는 바이트를 넣으면 - 예시 세 번째 줄의 0xCA 0xBF 쌍 - 출력은 순수한 ASCII(Tcq/)를 유지합니다. 127을 넘는 바이트는 그저 바이트이고, 테이블은 그것이 무엇을 뜻하는지 신경 쓰지 않으니까요. 40줄, 의존성 없음, 그리고 모든 엣지 케이스가 당신이 쓴 줄입니다. 이것이 전부가 바로 요점이죠.

Windows CryptoAPI: OS 내장 팩터

Windows에는 운영체제 자체 안에 base64 인코더가 있습니다. 이 글의 대부분의 프레임워크보다 오래된 것이요: wincrypt.h의 CryptBinaryToStringA, crypt32.lib 안에, 수십 년간 Windows와 함께 싣져 온 CryptoAPI의 일부입니다. 바이트 배열을 포맷된 문자열로 바꾸며, 그 플래그 테이블은 포맷 전체의 역사 같은 메뉴로 읽힙니다:

플래그 값 얻는 것
CRYPT_STRING_BASE64HEADER 0x0 인증서 BEGIN/END 헤더 줄로 감긴 Base64
CRYPT_STRING_BASE64 0x1 순수 base64, 헤더 없음
CRYPT_STRING_BASE64URI 0xD URL 안전 알파벳: +가 -로, /가 _로, RFC 4648 5절에 따라
CRYPT_STRING_NOCRLF 0x40000000 끝에 줄바꿈 추가 안 함
CRYPT_STRING_NOCR 0x80000000 기본 CRLF 대신 홀로 LF

먼저 알아 둘 것은 기본값입니다: CRYPT_STRING_NOCRLF를 넘기지 않으면, 함수는 문자열 끝에 캐리지 리턴/라인 피드 쌍을 붙여 줍니다 - 문서화된 동작은 모든 비바이너리 포맷에 줄바꿈 시퀀스가 붙는다는 것 - 그래서 한 줄에 맞아야 하는 base64 토큰은 BASE64 | NOCRLF를 원하고, 그 조합이 관용적 호출입니다. 둘째는 호출 규약으로, 클래식한 Windows 두 단계입니다: NULL 버퍼로 호출해 필요한 공간을 물어봅니다(답은 종료 NUL을 포함합니다), 할당하고, 다시 호출하고, NUL을 뺀 길이를 읽어 옵니다:

#include <windows.h>
#include <wincrypt.h>
#include <cstddef>
#include <string>

std::string win32_encode(const std::string &in,
                         DWORD flags = CRYPT_STRING_BASE64) {
  DWORD need = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            nullptr, &need))
    return {};
  std::string out(need, '\0');
  DWORD got = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            out.data(), &got))
    return {};
  out.resize(got);
  return out;
}

노트 둘 더. URI 플래그는 이 글 전체에서 유일한 네이티브 base64url입니다 - Windows에서는 토큰 알파벳을 직접 인코딩할 수 있고, 아래 섹션의 트랜스코드 접근은 엄밀히 다른 플랫폼용입니다. 그리고 값이 0인 CRYPT_STRING_BASE64HEADER 항목은, 제로를 넘기면 얻는 플래그이기도 하므로, 아무 플래그도 "의도하지" 않은 호출은 페이로드를 인증서 헤더 줄로 조용히 감싸 버립니다 - PEM 시대의 프레이밍 습관이죠. .pem 파일 생성에는 유용하고, 나머지에겐 놀람입니다. crypt32.lib에 링크하면, 그 함수는 프로그램 남은 생애 당신 것입니다.

Base64url: 토큰과 URL을 위한 알파벳

표준 알파벳에는 URL을 살아남지 못하는 두 문자가 있습니다: 쿼리 문자열에서 +는 공백을 뜻하고, 경로에서 /는 디렉터리를 뜻합니다. RFC 4648 5절은 두 문자 교환으로 이 문제를 고칩니다 - +가 -가 되고 /가 _가 되며 - 결과를 딱 끊어 말합니다: 이 인코딩은 "base64 인코딩과 동일하게 여겨져서는 안 된다". 이것은 JWT, OAuth PKCE 코드 챌린지, YouTube 동영상 식별자, 그리고 대부분의 API 토큰의 알파벳입니다. 토큰에서는 길이가 암묵적으로 알려져 있고 패드는 그냥 일어나기를 기다리는 퍼센트 이스케이프에 불과하기 때문에, = 패딩도 루틴하게 떨어뜨리죠.

이 글의 인코더 중에서 이 알파벳을 네이티브로 내뱉는 것은 Windows 플래그뿐입니다 - OpenSSL은 URL 안전 모드가 없고, Boost 두 갈래도 마찬가지 - 그래서 대부분의 플랫폼에서 레시피는: 표준으로 인코딩, 두 문자 교환, 패드 제거. 열두 줄이면 됩니다:

#include <cstddef>
#include <cstdio>
#include <string>

/* "40줄, 의존성 제로" 절에서 온 base64_encode */

std::string base64url_encode(const std::string &in, bool pad = false) {
  std::string out = base64_encode(in);
  for (char &c : out) {
    if (c == '+') c = '-';
    else if (c == '/') c = '_';
  }
  if (!pad)
    while (!out.empty() && out.back() == '=')
      out.pop_back();
  return out;
}

int main() {
  std::printf("%s\n", base64url_encode("M\312\277").c_str());
  std::printf("%s\n", base64url_encode("M").c_str());
  std::printf("%s\n", base64url_encode("M", true).c_str());
}

출력의 첫 줄은 Tcq_입니다. 표준 알파벳이 /를 썼을 자리가요. 뒤 두 줄은 패드 스위치가 작동하는 것을 보여 줍니다 - 기본적으로 패딩 없는 TQ, 소비자가 되돌려 원하면 TQ==. 그 pad 인자가 바로 생각해 볼 것인데, 소비자들이 의견을 달리하니까요: JWT 세그먼트는 패드를 원하지 않고, PKCE 챌린지도 패드를 원하지 않지만, 디코더가 길이에 엄격한 필드에 들어가는 base64url 값은 패드를 되돌려 원할 수 있습니다. 그리고 그 스위치는 bool이지, 재작성이 아니죠. 반대 방향으로 기억할 실패 모드도 있습니다: 표준 알파벳 페이로드 안의 -는 그냥 무효입니다. 그래서 두 알파벳은 바이트 수준에서 교환 가능하지 않습니다 - 잘못된 알파벳으로 인코딩한 토큰은 디코딩되지 않고, 실패합니다. 보안 경계에서 원하는 실패죠.

줄바꿈: 64, 76, 아니면 절대 안 함

줄바꿈된 base64에는 세상의 세 가지 줄 길이가 있고, 각각에 역사가 있습니다. OpenSSL 스트리밍 인코더는 64문자에서 고정입니다 - PEM의 습관이요. 1987년 프라이버시 강화 메일 표준이 64에서 줄바꿈했죠. 1993년에 이메일 인코딩을 표준화한 MIME은 76문자로 옮겼고, 그 숫자는 coreutils base64 명령의 기본값입니다(-w 플래그가 너비를 정하고, -w 0는 줄바꿈을 완전히 끄죠) 그리고 생태계 도구의 대부분 기본값이기도 합니다. RFC 4648 자체는 편을 들지 않습니다: 76을 MIME의 한도로 인용하고, 참조 규정이 지시하지 않는 한 아예 줄바꿈을 하지 말라고 구현에 말합니다. 당신이 어느 쪽을 내뱉을지는 누가 소비하느냐에 달려 있고, 설계 제약은 포맷이 아니라 소비자입니다.

줄바꿈은 인코딩된 문자열에 대한 후처리 단계이지, 결코 입력 단계가 아닙니다: 4문자 그룹이 의미의 단위이므로, 너비의 어떤 배수에서든 문자열을 자르는 것은 안전한 자르기입니다 - 모든 줄 경계가 그룹 사이에만 놓이니까요. C++ 버전은 루프입니다:

#include <cstddef>
#include <cstdio>
#include <string>

/* "40줄, 의존성 제로" 절에서 온 base64_encode */

std::string wrap_lines(std::string s, size_t width = 76) {
  std::string out;
  for (size_t i = 0; i < s.size(); i += width)
    out += s.substr(i, width) + "\r\n";
  return out;
}

int main() {
  std::string mime = wrap_lines(base64_encode(std::string(200, 'x')));
  int lines = 0;
  for (char c : mime)
    if (c == '\n') lines++;
  std::printf("mime: %d lines, %zu chars\n", lines, mime.size());
}

계산은 이렇습니다: 200바이트는 268문자로 인코딩되고, CRLF 종료자로 76에서 줄바꿈하면 4줄 - 풀 라인 세 줄과 40문자 꼬리 - 배선 위 276문자입니다. 스니펫의 CRLF 선택은 이메일의 선택입니다. 그 외 모든 것에는 LF가 현대의 기본값이고, 협상할 수 없는 단 하나의 규칙은 일관성입니다 - CRLF를 기대하는 디코더는 엄격하다면 홀로 남은 LF를 데이터 문자로 읽을 테니까요. (MIME의 규칙은 디코더가 줄바꿈을 무시해야 한다는 것이고, 그래서 이메일은 그 차이로 인한 일을 받은 적이 없습니다.) 알아 둘 세 번째 습관: openssl base64 명령 - 트렌치 코트를 걸친 enc 프로그램으로, argv[0]에서 자기 이름을 확인하는 것 - 은 -A 없이 64에서 줄바꿈하고 -A로 한 줄을 내뱉으며, 명령줄에서 실행할 때마다 확인하고 기억에서 신뢰하지 않는 유일한 도구입니다.

JSON과 설정 속 바이너리

JSON 문자열에는 그대로 넣을 수 없는 문자들의 짧은 목록이 있습니다: 따옴표, 백슬래시, 그리고 0x20 미만의 제어 문자. 인증서, 랜덤 키, 서명 - 전부, 원시 문자열 필드 안에 탄다 하면 연쇄 이스케이프로 변할 바이트로 가득하고, 제어 문자는 어떤 파서를 곧바로 목 막히게 만듭니다. Base64가 바로 해답이며, 바이너리를 실어 나라야 하는 모든 설정 포맷이 주는 기본 답이기도 합니다: 값은 순수 알파벳 문자의 한 줄로 저장되고, JSON 라이브러리의 따옴표 규칙에는 할 일이 남지 않습니다.

C++ 패턴이 전체 구현입니다: 바이트를 읽고(당연히 바이너리 모드로), 인코딩하고, 문자열을 저장합니다. 소비자는 반대편에서 디코딩하죠. JSON만의 함정은 하나, 줄바꿈된 문자열입니다: 76문자로 줄바꿈된 인증서를 JSON 파일에 그대로 붙여 놓으면, 문자 그대로 제어 문자로 가득한 문자열이 되며, 그것은 파서의 기분에 따라 파싱 오류이거나 조용한 손상입니다. 값이 사람의 눈에 보여서 줄바꿈되어야 한다면, 이스케이프해야 하거나 한 줄이어야 합니다 - 그리고 기계 대 기계 설정에는 한 줄이 답입니다. 다른 함정은 라벨 없는 값입니다: 2014년 man 페이지에서 base64라고 쓰는 설정 컬럼은 보통 패딩된 표준 알파벳이지만, API 시대의 토큰은 패딩 없는 URL 안전형이고, 디코딩 가이드의 4문자 검사 - +나 /가 들어 있나, -나 _가 들어 있나, 끝에 =가 있나? - 가 전체 진단입니다.

Data URI: 페이지에 붙여 넣는 파일

data URI는 페이로드가 바로 주소 안에 있는 URL입니다: data:, 선택적 미디어 타입, 선택적 ;base64 마커, 쉼표, 그리고 데이터 자체 - RFC 2397의 전체 스키마죠. 브라우저는 추가 요청 없이 이미지, 폰트, 작은 스크립트를 HTML과 CSS에 직접 넣어 두기 위해 이것을 사용하며, 네트워크를 끈 채에도 작동하는 페이지라면 data URI가 유력한 용의자입니다. C++ 쪽에서 인코딩 업무는 문자열을 조립하는 것으로, 상수 하나를 둔 문자열 연결입니다:

#include <cstdio>
#include <string>

/* "40줄, 의존성 제로" 절에서 온 base64_encode */

std::string make_data_uri(const std::string &mime_type,
                          const std::string &binary) {
  return "data:" + mime_type + ";base64," + base64_encode(binary);
}

int main() {
  std::printf("%s\n", make_data_uri("text/plain", "hi").c_str());
}

함정은 전부 디테일에 있습니다. ;base64 마커는 정확히 7문자이고, 오프바이원 버그가 노리는 길이입니다: 6자를 검사하는 파서는 data:text/plain;base4,...를 받아 들여서 쓰레기를 담담한 얼굴로 디코딩하는 파서죠. 그리고 base64 data URI의 페이로드는 한 줄입니다 - 줄바꿈은 URI 문법의 일부가 아니므로, 인코더가 이미지를 76에서 줄바꿈했다면(MIME 모양 인코더는 기본적으로 그렇게 합니다), 그 URI는 브라우저에 도달하기도 전에 깨져 있습니다. 이 소비자들을 위한 규칙: 인코딩하고, 줄바꿈하지 말고, 미디어 타입을 정확히 두세요 - JPEG에 틀린 image/png를 붙이는 것은, 자정 2시에 깨진 섬네일로만 드러나는 종류의 거짓말이니까요.

토큰: JWT, PKCE, API 키

인터넷에서 가장 베팅이 큰 base64는 토큰 안에 있습니다. JSON Web Token은 점으로 붙여진 base64url 세그먼트 3개입니다: 헤더 JSON, 클레임 JSON, 그리고 header.claims 문자열 위에서 계산된 서명. C++에는 내장 JWT 타입이 없지만, 하나를 만드는 것은 위의 base64url 인코더에 HMAC 호출 하나를 더하는 것입니다. 토큰 전체가 base64url인 채로, 서명이 되는 그 순간까지 계속되니까요:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>
#include <openssl/hmac.h>

/* 앞선 절들에서 온 base64_encode와 base64url_encode */

std::string jwt_hmac256(const std::string &signing_input,
                        const std::string &secret) {
  unsigned char digest[EVP_MAX_MD_SIZE];
  unsigned int len = 0;
  HMAC(EVP_sha256(), secret.data(), static_cast<int>(secret.size()),
       reinterpret_cast<const unsigned char *>(signing_input.data()),
       signing_input.size(), digest, &len);
  return std::string(reinterpret_cast<const char *>(digest), len);
}

int main() {
  const std::string header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  const std::string claims_json =
      "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";
  std::string head = base64url_encode(header_json);
  std::string claims = base64url_encode(claims_json);
  std::string signing_input = head + "." + claims;
  std::string sig = base64url_encode(jwt_hmac256(signing_input, "secret"));
  std::printf("token: %s\n", (signing_input + "." + sig).c_str());
}

예시를 돌리면 나오는 토큰은 교과서 같은 HS256 토큰입니다: 헤더는 {"alg":"HS256","typ":"JWT"}로, 클레임은 주제, 이름, 발급 시각 타임스탬프로 디코딩되고, 서명은 인코딩된 두 세그먼트 위 HMAC-SHA256의 base64url입니다. 세 디테일이 전체 설계를 지탱합니다. 서명 입력은 인코딩된 세그먼트이지, 원시 JSON이 아닙니다 - JSON을 서명하면 잘못된 바이트를 서명한 것이죠. 세그먼트는 패딩 없는 base64url입니다 - 패드가 URL 한가운데에 놓이겠고, 이 알파벳의 전부인 점은 토큰을 깨끗한 한 문자열로 유지하는 것이었으니까요. 그리고 HS256은 공유 시크릿을 뜻하며, 서버 대 서버 알고리즘입니다: 클라이언트 코드 속에 사는 시크릿은 시크릿이 아니고, 그것이 서명하는 토큰은 자격 증명이 아니죠. (OAuth의 PKCE 플로는 한 칸 건너에서 같은 알파벳을 씁니다: 랜덤 검증자를 SHA-256으로 해시하고, 패드 없이 base64url로 만들어 코드 챌린지로 - base64url 섹션의 인코더가 클라이언트 쪽 구현의 전부입니다.)

HTTP Basic 인증

HTTP에서 가장 오래된 base64는 자격 증명 헤더입니다: Authorization: Basic 뒤에 user:password의 base64가 오는 것으로, JSON보다 먼저 나온 만큼 오래된 스킴이죠. 만드는 것은 연결입니다:

#include <cstdio>
#include <string>

/* "40줄, 의존성 제로" 절에서 온 base64_encode */

std::string basic_auth_header(const std::string &user,
                              const std::string &pass) {
  return "Basic " + base64_encode(user + ":" + pass);
}

int main() {
  std::printf("%s\n", basic_auth_header("user", "password").c_str());
}

출력은 당신이 캡처한 요청에서 본 적 있는 문자열입니다: Basic dXNlcjpwYXNzd29yZA==. C++ 노트 둘. 연결 user + ":" + pass가 바로, 콜론을 포함한 비밀번호가 반대편의 순진한 파서를 당혹스럽게 만드는 자리입니다 - 파싱 규칙은 "첫 콜론에서 나눈다"이므로, 만드는 쪽은 어느 필드에든 무엇이든 넣을 자유가 있죠. 그리고 자격 증명이 ASCII가 아니라면, 이 스킴의 안전한 읽기는 base64 전에 사용자 ID와 비밀번호를 UTF-8로 다루는 것입니다. C++에서는 당신의 std::string이 이미 그 일을 하고 있다는 뜻인데, 조건은 로케일이 결정해 준 것이 아니라 UTF-8 바이트로 채워 넣는 것이죠. 이 스킴의 모든 언급에 보안 주의를 붙입니다: Basic 인증은 은폐이지, 보호가 아닙니다. 헤더는 네트워크를 읽을 수 있는 누구에게든 평문으로 타기 때문에 TLS 뒤에서만 허용할 수 있고, 그래도 기계 대 기계 호출용이지, 사람을 위한 선택지는 아닙니다. (Boost.Beast 코덱 - detail:: 네임스페이스의 그쪽 - 은 Boost의 WebSocket 구현 안에서 같은 헤더 일을 합니다. Sec-WebSocket-Accept 키가 될 SHA-1 다이제스트를 base64로 바꾸죠. 이 패턴이 2017년부터 이 일을 해 왔다는 조용한 증거입니다.)

이메일: 7비트 규칙, Base64의 답

이메일이 base64가 습관을 배운 곳이며, 그 습관들은 여전히 핵심 업무를 보고 있습니다. SMTP의 원형은 7비트 ASCII를 실어 나르도록 지어졌으므로, 바이너리인 것은 무엇이든 여행 전에 출력 가능한 텍스트로 다시 씌어져야 했습니다. 프라이버시 강화 메일이 1987년에 64자 줄과 끝에 붙여진 RSA-MD2/MD5 메시지 무결성 체크로 그것을 했고, 1993년에 이메일 인코딩을 표준화한 MIME은 한도를 76자로 완화하고, 표준 준수 디코더는 줄바꿈을 그저 무시해야 한다는 규칙을 추가했습니다. 이메일 첨부 파일은 오늘도 여전히 base64이며 76으로 감겨 있고, 정확한 계산은 4/3 곱하기 78/76, 즉 원래 크기의 약 137퍼센트, 그리고 대략 814바이트의 헤더가 더해집니다.

C++ 쪽은 인코더에 위의 줄바꿈 함수를 더한 것입니다 - 한 줄로 인코딩, CRLF로 76에서 줄바꿈, 끝. 이메일 특유의 디테일 둘: 마지막 줄은 뒤에 줄바꿈을 가질 수도, 갖지 않을 수도 있습니다(디코더는 그것을 무시해야 하므로 둘 다 합법이고 둘 다 흔합니다). 그리고 줄바꿈된 값은 JSON 값도, 환경 변수도, 토큰도 아닙니다 - MIME 바디에 속하는 덩어리이고, 다른 어디로든 옮기는 순간 줄바꿈은 습관이 아니라 버그가 됩니다. 반대 방향 - 76으로 감겨 도착한 첨부 파일 - 은 자매 가이드의 영토로, 네 C++ 디코더가 줄바꿈에 대해 네 가지 다른 의견을 가지는 곳입니다.

파일, 스트림, 그리고 2 기가바이트 상한

파일 인코딩은 디코딩 가이드의 파일 작업의 거울 이미지입니다: 바이너리 모드로 열고(Windows에서 텍스트 모드로 읽으면 CRLF 쌍이 단일 줄바꿈으로 번역되어, 인코더가 보기 전에 데이터가 바뀝니다), 바이트를 읽고, 인코딩하고, 바이너리로 씁니다. 작은 파일 버전은 한 함수면 됩니다:

#include <cstdio>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>

/* "40줄, 의존성 제로" 절에서 온 base64_encode */

std::string encode_file(const std::string &path) {
  std::ifstream in(path, std::ios::binary);
  if (!in) return {};
  std::vector<unsigned char> bytes{std::istreambuf_iterator<char>(in),
                                   std::istreambuf_iterator<char>()};
  return base64_encode(
      std::string(reinterpret_cast<const char *>(bytes.data()), bytes.size()));
}

int main() {
  std::string b64 = encode_file("/etc/hostname");
  std::printf("file -> %zu chars\n", b64.size());
}

상한이 바로 섹션 제목의 C++ 특유의 사실입니다: EVP API의 모든 길이 매개변수는 int입니다. 그래서 EVP_EncodeBlock 한 번의 호출로 인코딩할 수 있는 입력은 약 2 GB가 최대이고, 그 호출의 출력 버퍼 - 1.33배 더 큰 것 - 은 int에 아예 들어맞지 않습니다. 상한 이하에서는, 메모리에 들어 맞는 파일에 블록 API가 충분합니다. 상한을 넘거나, 메모리에 넣고 싶지 않은 파일에는 청킹을 해야 하고 - 청킹 규칙은 이 루프에 대한 base64 특유의 유일한 제약입니다: 청크는 3바이트의 배수여야 합니다. 그룹핑이 3개 단위이므로, 그룹 한가운데 청크 경계가 있으면 출력이 바뀌니까요. 3072 - 1024바이트 청크 셋 - 은 편안한 청크 크기이고, 루프는 이렇습니다:

#include <algorithm>
#include <cstddef>
#include <string>

/* "40줄, 의존성 제로" 절에서 온 base64_encode */

std::string encode_streamed(const std::string &data) {
  std::string out;
  for (size_t pos = 0; pos < data.size();) {
    size_t take = std::min<size_t>(3072, data.size() - pos);
    out += base64_encode(data.substr(pos, take));
    pos += take;
  }
  return out;
}

각 청크는 독립적으로 인코딩되고, 연결한 결과는 원샷 결과와 동일합니다 - 청킹을 안전하게 만드는 속성이 바로 그것이고, 3바이트 그룹핑에서 그대로 나옵니다. (인코더 섹션의 OpenSSL 스트리밍 컨텍스트는 같은 일을 하면서 64문자 줄바꿈을 공짜로 더 해주므로, 소비자가 MIME 모양을 원하면 맞는 도구입니다.) 그리고 출력 쪽은 입력 쪽과 같은 버짓을 가집니다: 10 GB 파일은 13.3 GB 문자열이 되므로, 버퍼 - 또는 쓰고 있는 파일 - 은 수학 섹션의 공식으로 크기를 잡고, int 상한은 2 GB 위에서는 청크 경로가 편의가 아니라 - 유일한 경로라고 말합니다.

환경 변수와 명령줄

환경 변수는 JSON 문자열과 같은 문제를 갖고 있고, 답은 더 나빕니다: NUL 바이트를 아예 실을 수 없고, 제어 문자도 친구가 아닙니다. 표준 트릭은 페이로드를 base64로 바꿔 셸을 살아남게 하는 것이고, C++에서 인코딩 방향은 한 줄입니다:

#include <cstdio>
#include <cstdlib>
#include <string>

/* "40줄, 의존성 제로" 절에서 온 base64_encode */

int main() {
  setenv("MY_PAYLOAD", base64_encode("hello, env").c_str(), 1);
  std::printf("env: %s\n", getenv("MY_PAYLOAD"));
}

환경에 놓이는 값은 aGVsbG8sIGVudg==입니다: 순수 알파벳, 셸에 안전하고, .env 파일에 안전하고, CI 대시보드에 안전하며, base64 디코더가 있는 어떤 기계에서든 디코딩 가능. 명령줄 자체는 디코딩 쪽과 같은 두 도구 이야기이고, 인코딩 방향 플래그를 씁니다: coreutils의 base64(새로운 배포판이 싣는 uutils 재구현일 수도; base64 --version으로 확인)는 기본적으로 76에서 줄바꿈하고, -w 0가 한 줄을 줍니다; openssl base64 - argv[0]에서 자기 이름을 확인하고 base64 모드로 전환하는 enc 프로그램 - 은 64에서 줄바꿈하고, 한 줄에는 -A를 받습니다:

# 한 줄, 토큰과 설정용
base64 -w 0 < payload.bin > payload.b64
openssl base64 -A < payload.bin > payload.b64

# 줄바꿈, 이메일과 텍스트 파일용
base64 < payload.bin > payload-76.b64
openssl base64 < payload.bin > payload-64.b64

둘 다 base64url을 네이티브로 쓰지 못하므로, 셸에서 만든 토큰은 URL에 들어가기 전에 트랜스코드 처리를 받습니다. 그리고 명령줄은 인코딩 쪽의 침묵 실패 습관이 가장 위험한 자리입니다: 소비자가 한 줄을 기대했는데 줄바꿈한 인코더는 오류를 내지 않습니다. 줄바꿈이 들어간 문자열을 그냥 만들어 줄 뿐이죠 - 지금 프로덕션에서 쫓고 있는 실패가 정확히 그것입니다. 소중하다면, 당신의 프로그램 안에서 인코딩하세요. 버퍼가 공식으로 크기를 잡고, 줄 모양을 당신이 제어하는 변수로 다루는 곳에서요.

함정들: C++판

  • 주문하지 않은 NUL. EVP_EncodeBlock은 페이로드 뒤에 NUL 종료를 붙입니다. man 페이지의 예: 16바이트 들어가, 24 인코딩 + NUL, 버퍼에 25, 24 반환. 추가 바이트를 버짓하고 반환 값으로 resize하지 않으면, 당신의 토큰은 제로 바이트로 끝납니다.
  • 하드 64. OpenSSL 스트리밍 API는 64문자에서 줄바꿈하고, 매 블록이 줄바꿈으로 끝나며, 그것을 바꿀 플래그가 없습니다. 한 줄 소비자에게 줄바꿈된 인코더 출력을 주는 것은 제어 문자 버그입니다.
  • 48바이트 블록. EVP_EncodeUpdate는 풀 48바이트 입력 블록에만 출력을 냅니다; 나머지는 EVP_EncodeFinal까지 컨텍스트 안에 앉습니다. 블록당 출력 65바이트 + NUL을 버짓하고, *outl을 "내 페이로드의 바이트"로 읽지 마세요 - 그것은 이번 호출이 쓴 바이트 수고, 작은 첫 호출에는 제로니까요.
  • 반복자의 없는 패드. Boost.Serialization 체인은 =를 절대 내뱉지 않습니다. 2002년 반복자가 "Mane"를 인코딩하면 6문자가 나옵니다. 패드를 직접 붙이지 않으면, 엄격한 소비자가 그 문자열을 거부합니다.
  • 요청하지 않은 CRLF. CryptBinaryToStringA는 CRYPT_STRING_NOCRLF를 넘기지 않으면 CR/LF 쌍을 붙입니다. 기본 플래그로 만든 base64 토큰은 마땅한 것보다 2문자 더 길고, 끝에서 두 번째 문자는 캐리지 리턴입니다.
  • NULL 호출은 NUL을 센다. Windows 크기 탐색은 종료 NUL을 포함한 필요한 길이를 돌려주고, 진짜 호출은 그것을 뺀 길이를 건네 줍니다. 둘을 뒤섞는 것은 클래식한 오프바이원이고, 버퍼를 한 바이트 지나게 쓰거나 마지막 문자를 잃습니다.
  • 중간에가 아니라 뒤에 줄바꿈. 줄바꿈은 인코딩된 문자열에 대한 후처리 단계입니다. 너비의 배수에서 자르세요 - 언제나 안전, 4문자 그룹마다 자기 완결적이므로 - 원시 바이트를 줄바꿈하는 것은 절대 금지, 줄바꿈이 그 자리에 있는 것이 아니니까요.
  • 3개 단위 청크. 큰 페이로드를 조각으로 인코딩하면, 조각 경계는 3바이트 그룹에 놓여야 합니다. 그렇지 않으면 그룹핑 - 그리고 출력 - 이 바뀝니다. 3072는 다정한 청크이고, 3071은 버그입니다.
  • int, size_t가 아니라. 모든 EVP 길이 매개변수는 int입니다. 단일 호출 상한은 약 2 GB 입력이고, 그 입력의 출력은 int에 아예 들어맞지 않습니다. 상한 위에서는 청크 또는 스트리밍 경로가 취향이 아닙니다.
  • signed char. unsigned 캐스트 없이 char *에서 싸면, char가 signed인 플랫폼에서 127을 넘는 바이트는 음수이고, 그것으로 테이블 인덱싱은 미정의 동작입니다. const unsigned char *는 의식이 아닙니다.
  • 줄바꿈된 JSON 문자열. JSON 파일에 붙여 넣은 76문자 줄바꿈 값은 문자 그대로 제어 문자의 문자열입니다. 한 줄이거나, 이스케이프되거나, 아니면 JSON에 없는 것입니다.
  • 패드는 계약. 패딩을 원하는 소비자(MIME, 대부분의 디코더)가 있고, 원하지 않는 소비자(JWT, PKCE, URL 속 토큰)가 있고, 빠지거나 비정형인 패드를 아예 거부하는 엄격한 소수가 있습니다. 패드는 장식이 아닙니다; 포맷 계약의 일부입니다.
  • 두 알파벳. 표준 알파벳 페이로드 안의 -나 _는 무효이고, URL 안전 알파벳 안의 +나 /는 무효입니다. 알파벳은 바이트 수준에서 교환 가능하지 않습니다 - 도착지에 맞는 것으로 인코딩하고, 의도적으로 트랜스코드하세요.
  • std::string과 strlen. std::string은 제로 바이트를 기꺼이 실어 나르지만, C 문자열을 레거시 API에 넘기는 순간 strlen은 첫 NUL에서 멈춥니다. 포인터와 길이를 넘기세요, 맨땅 포인터는 금지입니다.
  • 버짓. 출력은 입력의 4/3입니다: 입력이 1.5 GB면 출력은 2 GB - int 상한이기도 하죠. 받는 버퍼, 컬럼, 배선을 추측이 아니라 공식으로 버짓하세요.

C++는 Base64를 어떻게 갖게 됐나

포맷의 역사는 이 언어의 현대 시대보다 오래되고, C++의 이야기는 이 언어가 반복해서 그것을 싣지 않은 이야기입니다. 지금 MIME base64라고 부르는 인코딩의 첫 표준화된 사용은, 1987년에 64자 줄과 끝에 붙여진 RSA-MD2/MD5 메시지 무결성 체크로 제안된 프라이버시 강화 메일 프로토콜이었습니다; "base64"라는 이름 자체는 1993년에야 왔고, MIME 표준이 이름을 붙여 주었을 때의 일입니다. C++은 1998년에 C++98로 등장했습니다 - MIME보다 5년 늦게 - 그리고 이 언어의 개발자들이 가장 먼저 손이 간 base64 코드는 Rene Nyffenegger의 2004-2008 C 쌍으로, 2008년 12월 4일의 Stack Overflow 질문이 그것을 웹 전체로 퍼뜨렸습니다. 그 이야기에서 가장 좋은 부분: 한 답변이 Nyffenegger의 자기 페이지를 링크하고 라이선스 헤더 포함 구현을 통째로 가져왔고, 최고 득표 답변은 그의 해결책을 필드의 나머지 전체와 벤치마크했습니다. 민요에는 라이선스 헤더가 있고 - 작곡가 본인은 댓글에 한 번도 등장하지 않았습니다.

그 다음, 생태계는 생태계가 하는 일을 했습니다. 2002년, Robert Ramey의 Boost.Serialization이 반복자 어댑터를 싣고 나왔습니다 - C++ 도구상자에서 가장 오래된 base64로, 디코딩 방향으로는 엄격하고 인코딩 방향으로는 유명하리만치 패딩 없으며, 이미 강제하던 알파벳 규칙을 RFC 3548이 법제화하기 한 해 전의 것이죠. 2017년, Boost 1.66이 Beast를 가져왔고, 그와 함께 헤더 전용 코덱도 왔습니다. 오늘날에도 그 푸터에 Nyffenegger 기원 표시를 싣고 있죠. OpenSSL의 EVP_EncodeBlock과 친구들은 OpenSSL 릴리스마다 들어 있으므로, 이 일꾼은 이 언어가 표준에 들어가야 하는지 논쟁해 온 만큼이나 도구상자에 있었습니다. Windows에서는 이야기란 단순히 운영체제가 싣줬다는 것입니다: 함수 하나, 플래그 테이블 하나, 표준은 전혀 관여 안 했죠. 그 사이 표준 자체는 C++11, C++14, C++17, C++20, C++23(2024년 출판), 그리고 이제 C++26을 지나갔고, 하나같이 64문자 알파벳을 쳐다보고는 다음으로 넘어갔습니다. C++26의 기술적 내용은 2026년 3월 영국 Croydon의 ISO C++ 회의에서 완성되어 투표로 통과했고(114-12-3), 텍스트 코덱 업무를 위한 새로운 <text_encoding> 헤더를 실제로 추가합니다; 위원회의 뒤이은 회의들, 2026년 6월 Brno와 2026년 11월 브라질 Búzios에서는 C++26을 되돌아보는 것이 아니라 C++29 워크 드래프트를 엽니다. Base64는 표준에 없습니다. 표준 여덟 개, 30년, 텍스트 인코딩 헤더 하나 - 그리고 이제 위원회는 base64를 추가할 모든 가능한 구실을 가졌으면서도 전부 거절했습니다. C++에서 base64의 실무적 역사는, 그랬고, 여전히, 그 라이브러리들의 역사입니다: EVP 쌍 하나, Boost 두 갈래, Windows 플래그 하나, 그리고 당신이 소유한 40줄 스니펫.

알아 두면 좋은 골동품들

  • OpenSSL 스트리밍 인코더의 48바이트 블록은 어떤 RFC에도 등장하지 않는 숫자입니다. base64 그룹 16개로, 출력 줄이 정확히 64문자가 되도록 고른 것 - PEM의 습관이죠 - 2026년에도 1987년이 여전히 핵심 업무를 보는 마지막 장소 중 하나입니다.
  • Boost.Beast의 encoded_size는 수학 섹션을 constexpr 함수로 만든 것입니다: 4 * ((n + 2) / 3), 상수를 주면 컴파일 시점에 평가되죠. 표준 라이브러리는 이 한 줄을 갖지 못했습니다; Boost가 대신 detail:: 네임스페이스에 싣고 나온 것입니다.
  • 가장 작은 패딩된 base64는 4문자, QQ==입니다: 2문자 분장을 한 1바이트. 가장 작은 패딩 없는 것은 2문자, QQ입니다. 패드 수도 메시지입니다: 패드 둘은 마지막 그룹에 바이트 하나가 있었음을, 패드 하나는 둘이었음을, 패드 없이는 셋이었음을 뜻합니다 - 받는 쪽은 꼬리만으로 입력 길이를 되찾을 수 있죠.
  • MIME의 오버헤드 계산은 정확합니다: 4/3 곱하기 78/76. 그래서 이메일 첨부 파일은 원래 크기의 약 137퍼센트로 도착하고, 대략 814바이트의 헤더가 더 얹힙니다. 이 글의 모든 인코더가 같은 세금을 냅니다; 줄바꿈 너비는 청구 방식만 바꿀 뿐입니다.
  • 전형적인 libstdc++나 MSVC에서, std::string은 할당 대신 소 문자열 최적화로 작은 페이로드를 스택 버퍼에 실어 나릅니다. 9바이트 입력은 12문자로 인코딩되고, 힙에는 절대 손을 대지 않습니다. 당신의 토큰의 base64 형태가 글자 그대로 스택 프레임 안에 살 수 있습니다. 표준 라이브러리가 광고하지 않는 종류의 공짜 점심이죠.
  • 셸에서 손이 갈 수 있는 openssl base64 명령은 애초에 명령이 아닙니다. enc 프로그램이 argv[0]에서 자기 이름을 확인하고 성격을 바꾸는 것이죠. 문자열 비교로 만든 별칭, C에서 하는 C++식 일이요.
  • YouTube 동영상 식별자는 base64url입니다: 11문자, 패딩 없고, URL 근처에 +나 /는 아예 없습니다. 이 지구에서 가장 많이 시청되는 인코딩 포맷은, RFC 4648이 한 페이지에 들어 맞는 섹션으로 추가한 "URL 및 파일명 안전" 변종으로 돌아갑니다.
  • A 넷 - AAAA - 은 제로 바이트 3개를 인코딩합니다. A가 알파벳의 제로이니까요. 한 문자로만 이루어진 base64 덩어리를 본 적이 있다면, 이제 그것이 무엇을 말하고 있었는지 압니다: 아무것도.
  • 같은 함수 쌍은 2008년 Stack Overflow 질문의 답변들과, 기원 표시 푸터가 붙은 Boost.Beast 소스와, 수많은 사적 코드베이스의 헤더 파일들에 등장합니다. C++ 개발자에게 base64가 어디서 왔느냐고 물으면, 가장 정직한 답변은 "모르겠고, 인터넷도 모르죠"입니다.

다른 방향

방금 싸놓은 모든 것은 반대편에서 같은 도구상자로 풀리고, 풀기 쪽에는 자기만의 습관 세트가 있습니다: 꼬리를 제로로 채우는 원샷 OpenSSL 함수, 패딩된 입력에 스트리밍 디코더가 무엇을 돌려주느냐를 바꾼 2025년 버그 수정, 흘러간 문자에서 멈추고 아무 말도 하지 않는 Boost.Beast 디코딩, 스페이스 하나에 예외를 던지는 반복자, 상처를 준 정확한 바이트를 가리키는 40줄 엄격한 디코더. 전체 언패킹 이야기 - 네 디코더의 성질들, base64url 트랜스코딩, 파일, MIME의 76자 습관, 그리고 조용히 실패하는 두 명령줄 도구 - 은 자매 사이트의 C++ 디코딩 가이드에 있습니다. 가서 읽고 오세요, 그리고 돌아와서 큰 것을 싸 보세요. 전체 게임이 바로 그것입니다: 표준 라이브러리 없음, 줄바꿈과 NUL에 대해 네 가지 다른 의견을 가진 벤더 네 곳, 이 글의 모든 버퍼 크기를 잡는 공식 하나, 그리고 모든 받는 쪽이 환불 받는 33퍼센트 세금 하나. 좋은 패킹 되세요.

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

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