Decodificação Base64 em C++ (Cpp): um guia completo
Você pegou a string. Uma fita longa de letras e dígitos, o ocasional + ou /, talvez um ou dois = no final, e em algum lugar do seu ticket, contrato ou coluna de banco de dados, a promessa de que aquilo é Base64. Agora você precisa dos bytes originais de volta, em C++, e precisa deles corretos. A página inicial deste site passeia pelo formato em profundidade, então aqui vai só a versão curta: quatro caracteres do alfabeto carregam três bytes, uma cauda de um ou dois = marca onde os dados de verdade pararam, e a forma codificada sai cerca de 33 por cento maior que o original. Decodificar é a direção que encolhe, então um decodificador nunca precisa de mais memória do que o payload que já segura. Essa é uma propriedade genuinamente agradável, e uma das alegrias discretas de trabalhar nessa direção.
A maior manchete é que o próprio C++ não decodifica um único caractere por você. A biblioteca padrão teve trinta anos para desenvolver uma função base64 e gastou todos eles com outras coisas, então todo programa em C++ traz o próprio decodificador de um banco de três personalidades muito diferentes, mais a opção de escrever cerca de quarenta linhas suas. Um é um cavalo de batalha que carrega a internet desde os anos 1990, um é um tipo silencioso que para no meio da frase e nunca diz uma palavra sobre isso, e um é um chato que joga uma exceção em um único espaço solto. Uma vez que você sabe o que cada um perdoa, o que ele recusa e o que ele faz em silêncio nas suas costas, decodificar deixa de ser fonte de bugs misteriosos. Vamos abrir alguns pacotes.
A caixa de ferramentas: quatro decodificadores, quatro temperamentos
Aqui está o panorama, de relance. Os quatro lidam com o alfabeto padrão; as diferenças estão nas bordas, e as bordas são onde os bugs vivem.
| Decodificador | De onde ele vem | Modelo de erro | Especificidade para lembrar |
|---|---|---|---|
| OpenSSL EVP | <openssl/evp.h>, link -lcrypto |
Retorna -1 em entrada corrompida |
A versão one-shot enche a cauda com zeros |
| Boost.Beast | <boost/beast/core/detail/base64.hpp>, header-only |
Nenhum: ele só para | Nenhum canal de erro de qualquer tipo |
| Iteradores do Boost.Serialization | <boost/archive/iterators/binary_from_base64.hpp>, header-only |
Joga dataflow_exception |
Trata = como um valor zero de verdade |
| Suas próprias quarenta linhas | Em lugar nenhum: é sua | Sua escolha, até a posição do byte | Você é dono de todo caso de borda para sempre |
A instalação é um nome de pacote por distro. Para o OpenSSL: libssl-dev no Debian e Ubuntu, openssl-devel no Fedora e RHEL, openssl no Arch, e brew install openssl no macOS. Para o Boost, cuja release atual é a 1.92.0 de agosto de 2026 num projeto que constrói bibliotecas desde 1998: libboost-dev ou boost-devel. Os dois decodificadores do Boost abaixo são header-only, então não há nada para linkar. Se o seu projeto é baseado em CMake, o setup inteiro cabe em três linhas:
find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)
Uma nota de versão antes do código, porque ela muda o que o seu decodificador retorna. O OpenSSL 3.5, lançado em abril de 2025 como uma linha long-term-support, corrigiu um bug de verdade no decodificador streaming (mais em um instante), e a release de recursos mais nova 4.0, de abril de 2026, herdou a correção. Se o seu build trava numa versão 3.0 ou 3.3 antiga, leia o parágrafo de versão na seção OpenSSL abaixo antes de confiar no tamanho de uma cauda.
OpenSSL: o decodificador que você provavelmente já linka
Se o seu programa em C++ toca TLS, hashing ou certificados, o OpenSSL já está no binário, e as suas rotinas base64 EVP são os decodificadores mais testados em batalha do ramo. A função one-shot é uma única chamada:
int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);
Entregue a ele um buffer de caracteres base64 e o seu tamanho, e ele escreve os bytes decodificados em t. Ele corta o espaço em branco inicial, corta o espaço em branco final, as quebras de linha e os carriage returns, e então aplica regras sem negociação: nenhum espaço em branco interno, e o tamanho cortado tem que ser um múltiplo de 4. Cada quatro caracteres de entrada produzem exatamente três bytes de saída, e aqui está a parte que surpreende: os caracteres de padding são decodificados para seis bits zero, e a man page nota, com calma, que é responsabilidade de quem chama levar o padding final em conta. Ou seja, a função está fazendo a aritmética por você e depois adicionando, em silêncio, até dois bytes zero de brinde no final. O wrapper C++ idiomático esconde a matemática do buffer atrás de um std::string:
#include <cstddef>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>
std::string openssl_decode(const std::string &b64) {
std::vector<unsigned char> out(b64.size() * 3 / 4 + 4);
int n = EVP_DecodeBlock(out.data(),
reinterpret_cast<const unsigned char *>(b64.data()),
static_cast<int>(b64.size()));
if (n < 0) return {};
size_t pads = 0;
for (size_t i = b64.size(); i > 0 && b64[i - 1] == '='; --i) ++pads;
return std::string(reinterpret_cast<const char *>(out.data()),
static_cast<size_t>(n) - pads);
}
int main() {
std::string one = openssl_decode("TQ==");
std::printf("%zu bytes: %02x\n", one.size(),
static_cast<unsigned char>(one[0]));
std::string word = openssl_decode("TWFuZQ==");
std::printf("%zu bytes: %s\n", word.size(), word.c_str());
}
Rode e o zero-fill aparece exatamente onde a documentação prometeu: decodificar a string de quatro caracteres TQ== dá três bytes, a letra M mais dois zeros, antes do wrapper cortá-los. Dê a ele TWFuZQ== e você ganha os quatro bytes limpos de "Mane". Dê a ele um caractere fora do alfabeto e recebe de volta uma string vazia, porque a função respondeu com -1. Repare no que o std::string está fazendo em silêncio neste wrapper: ele acompanha o próprio tamanho e contém bytes zero com prazer, então um JPEG decodificado pode morar no seu tipo de texto e ser comparado, hasheado e repassado. Em C você estaria agradecendo por uma variável de tamanho; aqui a string só funciona.
Para dados que chegam em pedaços, o OpenSSL entrega a você um context que você alimenta com chunks e puxa resultados de, e as três funções têm um vocabulário compacto de valores de retorno:
| Chamada | Retorna | O que significa |
|---|---|---|
EVP_DecodeUpdate |
-1 |
Caractere inválido, ou um caractere de pad no meio dos dados |
EVP_DecodeUpdate |
1 |
Mais entrada é esperada |
EVP_DecodeUpdate |
0 |
Fim dos dados: o último grupo carregava padding, ou o marcador suave de fim de entrada apareceu |
EVP_DecodeFinal |
1 / -1 |
O stream terminou limpo / os caracteres residuais não eram um múltiplo de 4 |
Dois comportamentos fazem do decodificador streaming o mais permissivo da caixa de ferramentas. Ele pula espaços, tabs, carriage returns e quebras de linha em qualquer lugar do stream, então um bloco de email MIME com linhas CRLF de 76 caracteres passa exatamente como uma string de uma linha, e ele reporta a contagem de bytes verdadeira: o mesmo TQ== que enganou a função one-shot te dá exatamente um byte aqui, sem aritmética alguma. Ele mastiga a entrada em chunks de até 64 caracteres base64, trabalhando de um buffer interno de 80 bytes, e bufferiza o que não cabe num grupo de quatro, que é por isso que você pode alimentá-lo em tamanhos de pedaço arbitrários. O wrapper:
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>
std::string openssl_decode_stream(const std::string &b64) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
std::string out;
std::vector<unsigned char> chunk(1024);
int outl = 0;
for (size_t pos = 0; pos < b64.size(); pos += chunk.size()) {
size_t take = std::min(chunk.size(), b64.size() - pos);
int ret = EVP_DecodeUpdate(ctx, chunk.data(), &outl,
reinterpret_cast<const unsigned char *>(b64.data()) + pos,
static_cast<int>(take));
if (ret < 0) {
EVP_ENCODE_CTX_free(ctx);
return {};
}
out.append(reinterpret_cast<const char *>(chunk.data()), outl);
if (ret == 0) break;
}
unsigned char tail[3];
int tail_l = 0;
int fin = EVP_DecodeFinal(ctx, tail, &tail_l);
EVP_ENCODE_CTX_free(ctx);
if (fin != 1) return {};
out.append(reinterpret_cast<const char *>(tail), tail_l);
return out;
}
E agora a nota de rodapé de versão da tabela da caixa de ferramentas, porque é exatamente o tipo de coisa que reabre um ticket "resolvido": em toda release do OpenSSL antes da 3.5, o caminho streaming tinha o mesmo hábito de zero-fill que o decodificador de bloco. Foi reportado em fevereiro de 2025 como issue 26677; o commit da correção chegou no master em 27 de fevereiro de 2025, e o pull request associado foi fechado no mesmo dia. A man page oficial agora registra isso na seção de histórico: a partir do OpenSSL 3.5, EVP_DecodeUpdate produz a quantidade de bytes que a documentação sempre prometeu e não decodifica mais padding para bits zero. Se o seu codebase trava num OpenSSL antigo e os tamanhos da sua cauda parecem um ou dois bytes fora, essa é a primeira coisa a verificar. E há uma excentricidade herdada da era PEM: o hífen - não é um caractere do alfabeto de jeito nenhum - ele é um marcador suave de fim de entrada. Se o seu stream contiver um depois de um múltiplo de 4 caracteres válidos, o decodificador retorna 0 e pede para você parar, que é por isso que uma string base64url que precisa de verdade dos seus caracteres - não vai simplesmente decodificar - transcode primeiro, na seção abaixo, e o viajante do tempo desaparece.
Boost.Beast: o decodificador rápido que nunca reclama
Se o Boost já está no projeto, a biblioteca HTTP dele carrega um codec base64 no endereço improvável boost/beast/core/detail/base64.hpp. O namespace detail:: é o jeito do Boost de dizer "isso é assunto interno nosso", e os mantenedores se recusaram a promovê-lo a uma API pública. Todo mundo usa assim mesmo, porque ele é pequeno, rápido e header-only: defina BOOST_BEAST_HEADER_ONLY antes do include e não há nada para linkar. Ele é também, aliás, o codec que o próprio handshake WebSocket do Boost.Beast usa para o cálculo do Sec-WebSocket-Accept, então ele está mastigando tráfego de verdade há anos.
A personalidade da função de decodificação é a surpresa. Ela pega o seu buffer de saída, a entrada e o seu tamanho, e retorna um par: a quantidade de octetos escritos e a quantidade de caracteres lidos. Ela para no primeiro =, e no primeiro caractere inválido - e nos dois casos ela faz isso sem te avisar. Não há código de erro, não há exceção, não há flag de status. Um payload corrompido, um payload quebrado em linhas e uma cauda truncada produzem todos um resultado parcial bem-sucedido:
| Entrada | Bytes decodificados | Caracteres lidos | Por que parou |
|---|---|---|---|
"TWFuZQ==" |
4 bytes "Mane" |
6 | Parou no primeiro = |
"TWF!ZQ==" |
2 bytes "Ma" |
3 | Parou no ! |
"TWFu\nZQ==" |
3 bytes "Man" |
4 | Parou no \n |
"TWFuZQ" |
4 bytes "Mane" |
6 | A cauda foi truncada |
"TQ==" |
1 byte "M" |
2 | Padding tratado sem problemas |
Leia essa tabela uma segunda vez, porque ela é o modelo de ameaça inteiro de um decodificador que nunca reclama: ele fez o melhor que podia, parou onde parou, e fica com você perceber. A checagem tem um detalhe: para entrada com padding, a contagem de "caracteres lidos" para no primeiro =, então você soma os pads de volta antes de comparar, e também exige que o total seja um múltiplo de quatro, porque esse é o único formato que um payload de verdade tem:
#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <string>
namespace b64 = boost::beast::detail::base64;
std::string beast_decode(const std::string &in) {
std::string out;
out.resize(in.size() / 4 * 3 + 3); /* folga para tamanhos ímpares */
auto result = b64::decode(out.data(), in.data(), in.size());
out.resize(result.first);
size_t pads = 0;
for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
if (result.second + pads != in.size() || in.size() % 4 != 0)
return {}; /* parou cedo, ou a cauda era impossível */
return out;
}
Dois detalhes para arquivar. Primeiro, o helper decoded_size(n) para o qual o header te aponta é só um limite superior válido quando n é um múltiplo de 4 - o próprio comentário da função diz isso - que é por isso que o wrapper acima adiciona alguns bytes de folga em vez de confiar nele para tamanhos arbitrários. Segundo, procedência: os arquivos fonte carregam um copyright 2016-2019 de Vinnie Falco, com um rodapé atribuindo partes a um snippet de 2004-2008 de Rene Nyffenegger. Esse snippet é o par base64 que foi copiado e colado pela internet de língua inglesa há duas décadas, e agora ele vem dentro do Boost, no seu binário, fazendo HTTP Basic auth para a web inteira.
Boost.Serialization: o decodificador que joga uma exceção num espaço solto
A biblioteca de serialização do Boost carrega o base64 mais antigo do ecossistema C++: um conjunto de adaptadores de iteradores composáveis de 2002, escritos por Robert Ramey, que tratam "seja liberal no que você aceita" como um insulto pessoal. A direção de decodificação mora em binary_from_base64.hpp (sim, o nome é do ponto de vista da saída) e faz par com um transformador de largura que repacota valores de seis bits em bytes de oito bits:
#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <string>
namespace it = boost::archive::iterators;
std::string boost_decode(const std::string &in) {
using dec =
it::transform_width<it::binary_from_base64<const char *>, 8, 6>;
std::string out(dec(in.data()), dec(in.data() + in.size()));
size_t pads = 0;
for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
out.resize(out.size() - pads);
return out;
}
O iterador interno converte cada caractere base64 no seu valor de seis bits, e o externo reagrupa esses valores em bytes. O rigor dele é total: qualquer caractere fora do alfabeto - incluindo um único espaço - faz o iterador jogar boost::archive::iterators::dataflow_exception com a mensagem "tentativa de decodificar um valor que não está no conjunto de caracteres base64". Esse é o comportamento "rejeita a menos que se diga o contrário" que os RFCs depois tornaram explícito - implementado em 2002, um ano inteiro antes do RFC 3548 codificar a mesma regra. A consequência prática é que a entrada embrulhada em MIME precisa ter as quebras de linha removidas antes de tocar neste iterador. A segunda esquisitice é mais sutil: na tabela de consulta, o caractere de padding = não é pulado - ele é renderizado como o valor zero. Decodificar TWFuZQ== portanto produz seis bytes - 4d 61 6e 65 00 00 - porque os dois caracteres de pad contribuíram dados de verdade (zero), e a linha resize(size - pads) no snippet é carregante, não decorativa. Decodifique TQ== e você ganha três bytes que cortam para a única letra M, exatamente onde você quer aterrissar.
Quarenta linhas que são suas
Base64 é pequeno o suficiente para um decodificador correto ser uma coisa respeitável de se possuir, e no C++ o retorno é melhor do que em qualquer outra linguagem: std::string torna o gerenciamento de buffer agradável, e um decodificador feito à mão consegue fazer algo que nenhuma das versões de biblioteca acima faz, que é apontar para o byte exato que machucou. Esta versão segue a leitura estrita do RFC 4648 - corta as pontas, rejeita espaço em branco interno, rejeita padding no meio, impõe as regras de tamanho e até confere os bits de pad que o RFC diz que um codificador conformante deve ter zerado:
#include <cstddef>
#include <cstring>
#include <string>
std::string strict_decode(const std::string &in, size_t *error_pos = nullptr) {
static const char *table =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
auto fail = [&](size_t pos) {
if (error_pos) *error_pos = pos;
return std::string();
};
size_t start = 0;
size_t end = in.size();
while (start < end && (in[start] == ' ' || in[start] == '\t' ||
in[start] == '\r' || in[start] == '\n'))
++start;
while (end > start && (in[end - 1] == ' ' || in[end - 1] == '\t' ||
in[end - 1] == '\r' || in[end - 1] == '\n'))
--end;
size_t pads = 0;
while (end > start && in[end - 1] == '=') {
--end;
++pads;
}
size_t body = end - start;
if (pads > 2 || (pads == 1 && body % 4 != 3) ||
(pads == 2 && body % 4 != 2) || (pads == 0 && body % 4 == 1))
return fail(in.size());
if (body >= 2 && body % 4 != 0) {
int leftover = static_cast<int>((body % 4) * 6 % 8);
int last = static_cast<int>(std::strchr(table, in[end - 1]) - table);
if ((last & ((1 << leftover) - 1)) != 0)
return fail(end - 1); /* bits de pad não canônicos */
}
int value = 0;
int bits = -8;
std::string out;
out.reserve(body / 4 * 3);
for (size_t i = start; i < end; ++i) {
const char *p = std::strchr(table, in[i]);
if (!p)
return fail(i);
value = (value << 6) + static_cast<int>(p - table);
bits += 6;
if (bits >= 0) {
out.push_back(static_cast<char>((value >> bits) & 0xFF));
bits -= 8;
}
}
return out;
}
Passe pelo que ele impõe. Espaço em branco inicial e final é cortado, porque um payload copiado de um header de email tem toda a chance de chegar vestindo um. Espaço em branco interno é rejeitado, porque o RFC 4648 diz que um decodificador DEVE rejeitar caracteres fora do alfabeto a menos que a especificação envolvente diga o contrário, e numa fronteira de segurança você quer a leitura estrita. Um caractere de pad no meio dos dados é rejeitado, assim como qualquer tamanho que não possa corresponder a um payload de verdade: um caractere a menos de um grupo é impossível, e um único pad só é legal atrás de três caracteres de corpo. A verificação canônica no final é a que a maioria das implementações pula: se o último grupo tinha um ou dois caracteres de pad, os bits baixos não usados do caractere final do alfabeto devem ser zero, ou os mesmos bytes poderiam ser escritos como duas strings visivelmente diferentes. Essa maleabilidade é a razão de a verificação existir, e ela custa quatro linhas. Por fim, o decodificador aceita entrada sem padding, que é exatamente o que segmentos de JWT são. E na falha ele te entrega a posição: dê a ele TWF!ZQ== e o erro fica no índice 3, no ponto de exclamação, que é a diferença entre um bug report e uma correção.
Base64url: o alfabeto que os tokens falam
O alfabeto padrão tem dois caracteres que não sobrevivem a uma URL: + significa espaço numa query string, e / significa diretório num path. A seção 5 do RFC 4648 resolve isso com duas trocas de caracteres - + vira - e / vira _ - e é direto sobre o resultado: essa codificação "não deve ser considerada a mesma que a codificação base64". É o alfabeto dos JWTs, dos code challenges do OAuth PKCE, dos identificadores de vídeo do YouTube e da maioria dos tokens de API, e ele dispensa o padding = com frequência também, porque num token o tamanho é conhecido implicitamente e o padding seria apenas um percent-escape à espera de acontecer. Nenhum dos decodificadores C++ acima fala isso nativamente - o OpenSSL até trata um - como aquele marcador suave de fim de entrada - então a correção é um pequeno transcode antes de decodificar. É tão curto que é fácil guardar na cabeça:
#include <string>
std::string url_to_standard(std::string in) {
for (char &c : in) {
if (c == '-') c = '+';
else if (c == '_') c = '/';
}
switch (in.size() % 4) {
case 2: in += "=="; break; /* restaura o padding descartado */
case 3: in += '='; break;
default: break;
}
return in;
}
Aplique a um JWT de verdade e os dois primeiros segmentos são JSON puro à espera de acontecer. Um token de exemplo clássico decodifica para um header de {"alg":"HS256","typ":"JWT"} e um conjunto de claims contendo um subject, um name e um timestamp de emissão. O terceiro segmento decodifica do mesmo jeito e te entrega os bytes crus da assinatura - não é texto, e não é prova de nada. Decodificar um token te diz o que ele afirma; verificar a assinatura te diz se você deve acreditar, e isso é um trabalho de criptografia que nenhuma biblioteca base64 vai fazer por você. No Windows a situação é unilateral na mesma direção: CryptBinaryToStringA tem uma flag CRYPT_STRING_BASE64URI para o lado da codificação, mas a direção de decodificação não tem nenhuma flag URL-safe, então o transcode acima ganha o seu lugar na sua memória muscular em todas as plataformas.
De bytes de volta para texto
Pergunte a um decodificador C++ "que charset eu acabei de decodificar?" e você ganha a resposta mais honesta que a linguagem tem: nenhuma. Decodificadores são orientados a bytes do início ao fim. Eles não veem texto; veem bytes, e devolvem exatamente os bytes que foram empacotados. Se o original era UTF-8, você agora segura UTF-8, e nada mais é preciso. A virada agradável do C++ é que o std::string em si é um contêiner de bytes com um membro de tamanho, então o modo de falha clássico do C - uma função de string que para no primeiro NUL - em grande parte evapora. Um arquivo decodificado, um certificado decodificado, uma imagem decodificada: todos eles podem morar num string, serem comparados com ==, hasheados e passados por valor, e os bytes zero por dentro são simplesmente bytes. Só não converta para uma string C e depois meça com strlen; use size().
Para as codificações legadas que ainda se espreitam em bancos de dados antigos, exports e ferramentas escritas à mão - ISO-8859-1, Windows-1252 e seus parentes - a ferramenta padrão é o POSIX iconv, que vem com a glibc. Decodifique para bytes primeiro, depois converta esses bytes para UTF-8 com o codec que combina com a origem:
#include <iconv.h>
#include <cstddef>
#include <string>
#include <vector>
std::string to_utf8(const std::vector<unsigned char> &raw,
const char *source_charset) {
iconv_t cd = iconv_open("UTF-8", source_charset);
if (cd == (iconv_t)-1) return {};
char *inptr = reinterpret_cast<char *>(const_cast<unsigned char *>(raw.data()));
size_t inleft = raw.size();
std::vector<char> utf8buf(raw.size() * 4 + 8);
char *outptr = utf8buf.data();
size_t outleft = utf8buf.size();
if (iconv(cd, &inptr, &inleft, &outptr, &outleft) == (size_t)-1) {
iconv_close(cd);
return {};
}
iconv_close(cd);
return std::string(utf8buf.data(),
static_cast<size_t>(outptr - utf8buf.data()));
}
O round trip é sem perdas nas duas direções: empacote uma string como ISO-8859-1, base64, envie, decodifique, converta, e você ganha exatamente o com que começou, com os caracteres acentuados intactos. E dado binário não tem charset nenhum - um PNG é um PNG goste você ou não, que é a resposta mais libertadora de todo o artigo.
Decodificando arquivos
Arquivos pequenos são uma dança de quatro passos: abrir em binário, ler para um vector, decodificar, escrever o resultado de volta em binário. Modo binário, sempre, em toda plataforma - no Windows, uma leitura em modo texto traduziria pares CRLF em quebras de linha únicas e mudaria seus dados em silêncio antes que o decodificador sequer os visse:
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
std::vector<unsigned char> read_file(const std::string &path) {
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>()};
}
Repare nas chaves no código acima. Com parênteses simples, uma linha da forma std::vector<unsigned char> bytes(istreambuf_iterator<char>(file), istreambuf_iterator<char>()) é o infame "most vexing parse": o compilador lê como a declaração de uma função que retorna um vector, e está inteiramente correto em fazer isso. A forma com chaves acima contorna a gramática por completo. Uma vez que os bytes estão na mão, passe-os por qualquer decodificador deste artigo e escreva o resultado com std::ofstream em std::ios::binary, usando write(data.data(), data.size()) em vez do operador de stream, para que bytes zero embutidos sobrevivam à viagem até o disco. Um arquivo .b64 e o seu gêmeo decodificado então diferem exatamente pelo imposto de 33 por cento que você pagou na entrada, o que faz do checksum um momento satisfatório.
Arquivos grandes: stream nas duas direções
Para arquivos grandes demais para caber na memória, o decodificador streaming da seção OpenSSL faz o trabalho inteiro: ler um chunk, empurrar pelo context, escrever o que saiu, repetir. Apenas um buffer pequeno mora na RAM em cada momento, então um arquivo base64 de 10 GB decodifica com o mesmo código que um de 10 KB, e a quebra de linha estilo MIME não precisa de pré-processamento no caminho, porque o decodificador streaming dá de ombros para quebras de linha:
#include <fstream>
#include <string>
#include <vector>
#include <openssl/evp.h>
bool decode_stream_to_file(const std::string &in_path,
const std::string &out_path) {
std::ifstream in(in_path, std::ios::binary);
std::ofstream out(out_path, std::ios::binary);
if (!in || !out) return false;
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
std::string chunk(65536, '\0');
std::vector<unsigned char> decoded(49152);
bool ok = true;
for (;;) {
std::streamsize got = in.read(chunk.data(), chunk.size()).gcount();
if (got < 0) { ok = false; break; }
if (got == 0) break;
int outl = 0;
int ret = EVP_DecodeUpdate(ctx, decoded.data(), &outl,
reinterpret_cast<const unsigned char *>(chunk.data()),
static_cast<int>(got));
if (ret < 0) { ok = false; break; }
out.write(reinterpret_cast<const char *>(decoded.data()), outl);
if (ret == 0) break;
}
unsigned char tail[3];
int tail_l = 0;
if (ok && EVP_DecodeFinal(ctx, tail, &tail_l) != 1)
ok = false;
if (ok)
out.write(reinterpret_cast<const char *>(tail), tail_l);
EVP_ENCODE_CTX_free(ctx);
return ok;
}
Os tamanhos dos buffers não são arbitrários: os parâmetros de tamanho das funções EVP são int, então uma única chamada é segura até 2 GB, e os números acima mantêm cada chunk com 64 KB de entrada e um buffer de saída de 48 KB, que é exatamente 3 em 4. Esse teto int é a razão inteira de o caminho streaming existir, e vale a pena saber como um fato duro em vez de descobrir como um bug de plataforma. Se a entrada por acaso sair corrompida, a função retorna false no primeiro chunk que não pode ser decodificado, e o arquivo de saída segura o que era válido antes - o que, dependendo do seu pipeline, pode ser exatamente o resultado parcial que você queria.
HTTP, APIs e os campos JSON que escondem bytes
Base64 aparece no HTTP em duas formas. A primeira é dados: uma resposta JSON com um campo "certificate" ou "avatar" cheio de base64, um endpoint de upload que aceita os bytes numa coluna segura para texto, um endpoint de download que te entrega um arquivo .b64. O padrão é sempre o mesmo - parse o JSON, puxe a string, decodifique, trate o resultado como bytes - e o lado da decodificação deste artigo é a implementação inteira. A segunda forma é credenciais: o header Authorization: Basic é o base64 de user:password, e ele é o único caso de uso base64 do padrão há trinta anos. O parse disso é em dois passos, e o primeiro é onde as pessoas pegam uma string C terminada em NUL no meio de dados vizinhos de binário e se perguntam por quê:
#include <optional>
#include <string>
/* strict_decode da seção "Quarenta linhas que são suas" */
std::optional<std::pair<std::string, std::string>> parse_basic_auth(
const std::string &b64) {
std::string raw = strict_decode(b64);
size_t colon = raw.find(':');
if (colon == std::string::npos)
return std::nullopt;
return std::make_pair(raw.substr(0, colon), raw.substr(colon + 1));
}
Passe a ele o valor do header depois do prefixo Basic e ele te entrega o user e a password como strings de verdade com tamanho acompanhado, ou nada, se o payload não for um par user:pass. A nota de segurança pertence aqui mesmo não sendo um tema de C++: Basic auth é ofuscação, não proteção. O header viaja em claro para qualquer um que consiga ler a rede, então só é aceitável atrás de TLS, e mesmo assim é a escolha para chamadas máquina a máquina, não para pessoas.
JWTs: lendo o que um token afirma
Um JSON Web Token são três segmentos base64url colados com pontos: header, claims, assinatura. Os dois primeiros são objetos JSON; o terceiro é uma assinatura criptográfica sobre a string header.claims, calculada com o algoritmo nomeado no header. C++ não tem um tipo JWT embutido, mas ler um token não precisa de mais nada além do transcode da seção base64url e de um decodificador, porque a parte interessante é a leitura:
#include <cstddef>
#include <cstdio>
#include <string>
/* url_to_standard e strict_decode das seções anteriores */
int main() {
const std::string token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
size_t dot1 = token.find('.');
size_t dot2 = token.find('.', dot1 + 1);
std::string header = strict_decode(
url_to_standard(token.substr(0, dot1)));
std::string claims = strict_decode(
url_to_standard(token.substr(dot1 + 1, dot2 - dot1 - 1)));
std::printf("header: %s\n", header.c_str());
std::printf("claims: %s\n", claims.c_str());
}
O header volta como {"alg":"HS256","typ":"JWT"} e os claims como {"sub":"1234567890","name":"John Doe","iat":1516239022} - o subject, o name e um timestamp de emissão. Esse é o lado da leitura inteiro, e é genuinamente útil: registrar o que um token afirma, depurar um 401 olhando o campo de expiração ou decidir em quais claims confiar é tudo a uma decodificação de distância. O que não é é verificação. O segmento da assinatura é base64url também, e decodificá-lo te dá 32 ou 64 bytes crus que não provam nada sozinhos; a assinatura só faz sentido quando você recalcula o hash de header.claims com o segredo compartilhado ou a chave pública e compara. Trate um JWT decodificado como você trata uma carta: ela diz o que diz, e verificar o lacre é um trabalho separado, criptográfico.
Data URIs: arquivos que se colaram dentro de uma página
Uma data URI é uma URL cujo payload está ali mesmo no endereço: data: seguido de um tipo de mídia opcional, um marcador ;base64 opcional, uma vírgula, e então os dados em si - o esquema inteiro do RFC 2397. Navegadores usam para embutir imagens, fontes e scripts pequenos diretamente em HTML e CSS sem request extra, e se você um dia vir uma página que continua funcionando com a rede desligada, uma data URI é uma suspeita forte. No lado do C++, o trabalho de decodificação é dividir a URI e então passar o payload pelo seu decodificador de costume, porque quando o marcador ;base64 está presente o payload é base64 padrão puro - geralmente com padding, geralmente numa linha só:
#include <cstddef>
#include <string>
std::string data_uri_payload(const std::string &uri, bool *is_base64) {
const std::string prefix = "data:";
if (uri.rfind(prefix, 0) != 0)
return {};
size_t comma = uri.find(',');
if (comma == std::string::npos)
return {};
std::string meta = uri.substr(prefix.size(), comma - prefix.size());
*is_base64 = meta.size() >= 7 &&
meta.compare(meta.size() - 7, 7, ";base64") == 0;
return uri.substr(comma + 1);
}
Chame com data:image/png;base64,iVBORw0KGgo=... e ele te entrega o payload mais a flag dizendo qual caminho de decodificação tomar. Dois perigos. Primeiro, quando o marcador está ausente o payload é texto URL-encoded, não base64, então a flag não é uma formalidade - uma URI que parece base64 mas foi gerada como texto percent-encoded vai decodificar para lixo. Segundo, alguns geradores quebram data URIs longas com quebras de linha do jeito que o MIME faz; o seu decodificador estrito vai recusar aquelas, então remova as quebras de linha antes de decodificar se a fonte não estiver sob o seu controle. Decodificar o payload de uma URI data:image/png te dá os bytes exatos do PNG, header e tudo, que é a satisfação discreta de todo o exercício.
Email, MIME e o hábito dos 76 caracteres
Email é a razão pela qual o base64 aprendeu a quebrar as linhas. O SMTP, na sua forma original, foi construído para carregar ASCII de sete bits, então qualquer coisa binária tinha que ser reescrita como texto imprimível antes de poder viajar. O Privacy-Enhanced Mail fez isso em 1987 com linhas de 64 caracteres, e o MIME, quando padronizou a codificação para email em 1993, relaxou o limite para 76 caracteres e adicionou a regra de que um decodificador conformante deve simplesmente ignorar as quebras de linha. O hábito sobreviveu: um anexo de email ainda é base64 hoje, quebrado em 76, e a aritmética exata dá 4/3 vezes 78/76 - cerca de 137 por cento do tamanho original, mais algumas centenas de bytes de headers. O seu decodificador C++ encolhe tudo de volta para 100 por cento, que é o ponto de todo o formato.
O detalhe no C++ é que os decodificadores deste artigo não concordam sobre quebras de linha, e cada um tem uma razão. O decodificador streaming do OpenSSL pula em qualquer lugar do stream, que é exatamente a regra do MIME. A função one-shot do OpenSSL recusa qualquer espaço em branco dentro do payload. O Boost.Beast para na primeira quebra de linha sem dizer. Os iteradores do Boost jogam exceção num único espaço. Então quando um payload vem de email, sua primeira decisão é qual decodificador usar, ou você remove as quebras de linha você mesmo - uma passada de uma linha com erase-remove por cima de \r e \n - e deixa qualquer decodificador que você goste fazer o trabalho de verdade. Remover antes é a escolha chata e confiável, e é a que mantém a sua escolha de decodificador independente do histórico do seu payload.
Bancos de dados, arquivos de config e variáveis de ambiente
O terceiro lar do base64 é a camada de armazenamento: uma coluna num banco de dados legado cuja documentação diz "base64" e nada mais, um blob de config num arquivo JSON, um payload numa variável de ambiente que um serviço codificou em base64 para que ele sobrevivesse a uma shell. O padrão de decodificação é o mesmo de todo lugar - leia a string, decodifique, trate como bytes - mas a entrada sem rótulo merece um parágrafo especial, porque às vezes você realmente não sabe qual alfabeto foi usado. Você não pode saber, mas pode testar, porque quatro caracteres fazem a maior parte do trabalho:
- Contém
+ou/- só o alfabeto padrão pode estar certo. - Contém
-ou_- só o alfabeto URL-safe pode estar certo. - Nenhum dos dois, mas termina em
=- padrão com padding, ou uma string URL-safe com padding cujo payload nunca precisou dos dois caracteres trocados. - Nenhum dos dois, sem padding - pode ser qualquer um; a forma URL-safe crua é a comum na web, então para dados nascidos numa URL ou num token, tente essa primeiro.
Strings que não usam nenhum dos quatro caracteres de distinção decodificam de forma idêntica nos dois alfabetos, então para essas a ordem em que você as tenta é uma questão de onde os dados vieram: coisas nascidas em email querem o alfabeto padrão, coisas nascidas numa URL querem o URL. E lembre-se de tentar a leitura com padding e sem padding da mesma string - um = faltante é a diferença entre "recusado" e "resolvido", que é por isso que o decodificador estrito acima aceita os dois.
A linha de comando tem dois Base64s
Para trabalhos pontuais, uma máquina Linux geralmente tem dois decodificadores base64, e eles se comportam diferente exatamente do jeito que morde as pessoas. O primeiro é o base64 do GNU coreutils (algumas distribuições mais novas trazem a reimplementação uutils no lugar, e ambos falam as mesmas flags - confira com base64 --version). Ele é conformante com o RFC 4648, quebra em 76 caracteres ao codificar (com -w 0 desligando isso), e na decodificação aceita quebras de linha em qualquer lugar com prazer; a flag -i torna a tolerância a lixo explícita em vez de acidental. O segundo é o do OpenSSL, e aqui está a virada: openssl base64 não é um app próprio de jeito nenhum. Desde a série 1.1.0 (2016), o programa enc confere o próprio nome de invocação, e se foi chamado como "base64" ele se troca para o modo base64 - uma comparação de string em argv[0], que é o jeito C de entregar um alias. Sem -A, ele espera uma quebra de linha em algum lugar dos primeiros 1024 bytes de entrada, então uma string longa de uma linha volta vazia, com exit code 0. Com -A ele lê uma linha, e a lista de bugs documentados do comando enc é um museu de dois itens: a opção -A não funciona corretamente com arquivos grandes, e sem -A, se os primeiros 1024 bytes não contêm quebra de linha, as duas primeiras linhas de entrada são ignoradas. Num pipeline, um arquivo vazio em silêncio parece exatamente com uma decodificação bem-sucedida de um payload vazio.
# os one-liners honestos
base64 -d < payload.b64 > payload.bin
openssl base64 -d -A < payload.b64 > payload.bin
Nenhum fala base64url nativamente, que é mais uma razão de o snippet de transcode pertencer à sua memória muscular. Para qualquer coisa que importe, decodifique no seu programa, onde os erros voltam como números que você pode testar e o exit code de uma ferramenta em silêncio não é o seu único sinal.
Armadilhas que mordem especificamente o C++
- O zero-fill.
EVP_DecodeBlockretorna três bytes paraTQ==: a letra M mais dois zeros. Recupere o tamanho real do padding, ou use a API streaming, que é honesta sobre a contagem. - A esquisitice streaming pré-3.5. Nas releases do OpenSSL anteriores à 3.5.0 (abril de 2025),
EVP_DecodeUpdatetinha o mesmo hábito de zero-fill. Código escrito contra um pin de 3.0 ou 3.3 pode estar mentindo para você sobre tamanhos de cauda; a correção está registrada na seção de histórico da man page. - A parada silenciosa. O
decodedo Boost.Beast não tem canal de erro: para em qualquer caractere inválido, qualquer quebra de linha e qualquer tamanho de cauda impossível, e retorna um resultado parcial com a cara impassível. Confira queconsumed + pads == input.size()e que o total é um múltiplo de 4, ou você está decodificando o que ele decidiu decodificar. - A armadilha do decoded_size.
b64::decoded_size(n)assume quené divisível por 4. Dois caracteres de entrada podem produzir um byte, enquantodecoded_size(2)diz zero - adicione folga para tamanhos ímpares. - Os pads de byte zero. Os iteradores do Boost decodificam
=como o valor zero, entãoTWFuZQ==vira seis bytes incluindo dois zeros finais. Subtraia a contagem de pads, ou aproveite seus fantasmas. - Espaço em branco, de quatro jeitos. O OpenSSL streaming pula, o OpenSSL one-shot recusa internamente, os iteradores do archive jogam exceção nele, e o Beast para nele. Strings coladas adoram carregar espaço em branco, e cada decodificador tem a sua opinião sobre isso.
- Índice com char assinado. Se você montar a sua própria tabela de decodificação indexada por caractere, indexe com
unsigned char. Em plataformas ondecharé assinado, um byte acima de 127 vira um índice negativo, que é comportamento indefinido vestindo jaleco de laboratório. - O sinal de menos é um viajante do tempo. No OpenSSL,
-é um marcador suave de fim de entrada da era PEM, não um caractere do alfabeto. Transcode base64url antes de decodificar. - int, não size_t. Os parâmetros de tamanho do EVP são
int. Acima de 2 GB, só o caminho streaming em chunks é seguro, que é por isso que ele existe. - Modo texto no Windows. Abrir um arquivo para texto traduz CRLF para LF e corrompe sua entrada antes de decodificar.
std::ios::binary, sempre, em toda plataforma. - O most vexing parse.
std::vector<char> v(istreambuf_iterator<char>(f), istreambuf_iterator<char>())é uma declaração de função. Use inicialização com chaves ou um par de ponteiros. - Bits de pad não canônicos. Um decodificador tolerante pode aceitar strings cujos bits de pad não usados são diferentes de zero, então duas strings visivelmente diferentes decodificam para os mesmos bytes (maleabilidade do base64). Em fronteiras de segurança, rejeite o que você não precisa - o RFC 4648 diz que decodificadores podem fazer exatamente isso.
- A linha de comando falha em silêncio.
openssl base64 -dsem-Aengole entrada de uma linha (saída vazia, exit 0); os bugs documentados cobrem arquivos grandes e entrada sem quebra de linha nas duas direções. Confira a sua saída em pipelines. - strlen em binário.
std::stringmantém bytes zero felizes, mas no momento em que você entrega uma string C para uma API legada,strlenpara no primeiro NUL. Passe tamanho e ponteiro, nunca um ponteiro nu.
Uma breve história do Base64 em C++
O formato é mais antigo que a era moderna da linguagem. O primeiro uso padronizado da codificação hoje chamada de base64 do MIME foi o protocolo Privacy-Enhanced Mail, proposto em 1987 com linhas de 64 caracteres e uma verificação de integridade de mensagem RSA-MD2/MD5 colada no final; o nome "base64" em si só chegou em 1993, quando as especificações do MIME o batizaram. O C++ chegou ao cenário como C++98 em 1998 - cinco anos depois do MIME - e o primeiro código base64 ao qual os desenvolvedores da linguagem recorreram foi o par em C de Rene Nyffenegger (2004-2008), que uma pergunta no Stack Overflow de 4 de dezembro de 2008 espalhou pela web. A melhor parte dessa história é quem não apareceu: o autor nunca postou uma resposta ele mesmo, mas o snippet dele virou a canção folclórica que todo mundo copiou. Uma resposta na thread reimprimiu a implementação completa dele do próprio site dele, caso o site caísse.
Depois o ecossistema fez o que ecossistema faz. Em 2002, o Boost.Serialization de Robert Ramey trouxe os adaptadores de iteradores - o base64 mais antigo da caixa de ferramentas do C++, estrito a ponto de jogar uma exceção num único espaço, um ano antes do RFC 3548 codificar a regra que ele já aplicava. Em 2017, o Boost 1.66 trouxe o Beast, e com ele o codec header-only que ainda vem até hoje com o crédito de Nyffenegger no rodapé. Enquanto isso, o padrão em si foi C++11, C++14, C++17, C++20 e C++23 (publicado em 2024), e cada um deles olhou para o alfabeto de 64 caracteres e seguiu em frente. O C++26 adiciona um novo header <text_encoding> para trabalho de codec de texto, aprovado já em 2022; o conteúdo técnico dele terminou na reunião ISO de março de 2026 em Londres, onde o comitê votou 114-12-3 para enviá-lo para publicação, e as reuniões seguintes do comitê em 2026 - incluindo a de Búzios, Brasil, de 16 a 21 de novembro - são gastas abrindo o próximo working draft, o C++29, em vez de votar neste. Base64 nunca esteve no draft. Sete padrões, três décadas, um header para codificação de texto - e o comitê agora teve todas as desculpas possíveis para adicionar base64 e recusou todas. A história prática do base64 em C++ é, e continua sendo, a história das suas bibliotecas: as rotinas EVP do OpenSSL, dois sabores do Boost, uma chamada de API do Windows e um snippet de quarenta linhas que é seu.
Fatos curiosos, edição C++
- O mesmo par de funções aparece nas respostas a uma pergunta de 2008 no Stack Overflow, na fonte do Boost.Beast com um rodapé de crédito, e nos headers de incontáveis codebases privados. Pergunte a um desenvolvedor C++ de onde veio o base64 dele e a resposta mais honesta é "eu não sei, e a internet também não".
- Os iteradores do archive do Boost são o base64 mais antigo deste artigo, copyright 2002 - o mesmo ano em que o SDK do .NET Framework 1.0 saiu. Eles jogam uma exceção num único espaço, o que significa que estavam aplicando a regra "rejeitar caracteres fora do alfabeto" antes que os RFCs alcançassem: o RFC 3548 codificou em 2003, e o RFC 4648 repetiu em 2006.
- O decodificador streaming do OpenSSL trabalha de um buffer interno de 80 bytes, mas esvazia a cada 64 caracteres base64, a mesma largura de linha que a armadura PEM usa desde 1987. Esse 64 discreto é um dos últimos lugares onde o formato antigo ainda está fazendo trabalho de sustentação em 2026.
- O base64 com padding menor possível são quatro caracteres,
TQ==: um byte vestindo um disfarce de dois caracteres. O menor sem padding são dois caracteres,TQ. Qual deles você vai decodificar depende inteiramente de quem codificou, e essa pessoa não estava pensando em você. - A matemática do MIME é exata: 4/3 vezes 78/76, que é por isso que um anexo de email chega com cerca de 137 por cento do tamanho original (mais algumas centenas de bytes de headers por cima). O seu decodificador C++ encolhe de volta para 100 por cento, que é a alegria discreta de todo o exercício.
- Numa libstdc++ ou MSVC típica,
std::stringcarrega payloads pequenos num buffer da pilha através da small-string optimization em vez de alocar. Uma entrada de 9 bytes decodifica para 6 bytes e nunca toca o heap. A forma base64 do seu blob de config minúsculo pode literalmente morar num stack frame, que é o tipo de almoço grátis que a biblioteca padrão não anuncia. - O comando
openssl base64ao qual você pode recorrer numa shell não é um comando de jeito nenhum. É o programaencconferindo o próprio nome emargv[0]e trocando de personalidade. Um alias por comparação de string, que é o jeito C++ de fazer as coisas, em C. - Identificadores de vídeo do YouTube são base64url: onze caracteres, sem padding, nenhum
+ou/por perto de uma URL. O formato de codificação mais assistido do planeta roda na variante "Seguro para URL e Nome de Arquivo" que o RFC 4648 adicionou numa seção que cabe numa página.
Quando você precisar empacotar em vez disso
Tudo o que você acabou de decodificar foi empacotado pela mesma caixa de ferramentas do outro lado: EVP_EncodeBlock para one-shots, EVP_EncodeUpdate mais EVP_EncodeFinal para streams (e é daí que vêm aquelas linhas de 64 caracteres), a mesma aritmética de buffer invertida, e o mesmo imposto de 33 por cento que a decodificação devolve em silêncio. A história completa do empacotamento - a matemática de tamanho itemizada, os codificadores que terminam a saída com NUL, o iterador do Boost que nunca encontrou um caractere de pad, base64url, embrulho MIME, arquivos e a API do Windows com o seu hábito de CRLF - mora no guia de codificação C++ no site irmão. Vá ler, depois volte e abra algo grande. Esse é o jogo inteiro: sem biblioteca padrão, três fornecedores confiáveis com três temperamentos diferentes, um decodificador que aponta para o byte exato que machucou, um bugfix de 2025 que mudou a cauda do streaming e uma triplet de zeros para lembrar para sempre. Bom desempacotamento.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em C++ (Cpp): um guia completo