Codificação Base64 em C: um guia completo
Você tem bytes. Talvez sejam um JPEG lido do disco, talvez um token, talvez uma senha que um cliente está prestes a transmitir, talvez os bytes crus de um arquivo que alguma pipeline espera dentro de um campo JSON. E em algum ponto da estrada há um canal que só fala texto: uma string JSON, uma URL, o corpo de um email, um arquivo de configuração, uma coluna de banco de dados que finge ser texto. É aí que entra a codificação Base64: ela reescreve cada três bytes de dados crus como quatro caracteres de um alfabeto de 64 letras, de modo que o resultado é ASCII puro que sobrevive a qualquer pipeline de texto do planeta. A página inicial deste site explica o formato por inteiro; este artigo é sobre fazer o trabalho bem em C, onde - como sempre - a linguagem não vai fazer isso por você.
O único número para guardar na cabeça: codificar faz os seus dados crescerem. Três bytes viram quatro caracteres, então todo payload sai do seu programa mais ou menos um terço maior, mais um pouquinho sempre que quebras de linha são adicionadas. Esse é o imposto, e não tem como fugir dele - mas em C o imposto tem uma linha de custo específica, porque você aloca o buffer de saída você mesmo e o buffer precisa ser exatamente grande o suficiente. Acerte a matemática uma vez e todo codificador deste artigo fica previsível: sem estouro, sem falha por falta, sem se perguntar onde o próximo byte vai cair. Aí vem a caixa de ferramentas para escolher - OpenSSL, Mbed TLS, APR-Util, GLib - e a escolha importa, porque cada uma quebra linhas de forma diferente, termina diferente, e erra diferente.
Quatro codificadores, quatro personalidades
As quatro bibliotecas codificam o alfabeto padrão corretamente e de forma idêntica - os mesmos bytes entram, os mesmos caracteres saem, sempre. As diferenças estão na embalagem, e a embalagem é onde os bugs de interoperabilidade se escondem. Aqui está o cenário:
| Biblioteca | Cabeçalho | Estilo de saída | Modo de falha |
|---|---|---|---|
| OpenSSL (libcrypto) | <openssl/evp.h> |
Sem quebras de linha; escreve um terminador NUL | Praticamente nenhum (só alocação) |
| Mbed TLS | <mbedtls/base64.h> |
Sem quebras de linha; terminado em NUL | Código de buffer pequeno demais com o tamanho necessário |
| APR-Util | <apr-1.0/apr_base64.h> |
Sem quebras de linha; acrescenta um NUL | Nenhum - confie no tamanho do seu buffer |
| GLib | <glib.h> |
Sem quebras de linha; terminado em NUL, alocado no heap | Retorna NULL (só alocação) |
Note o que falta na tabela: nenhuma delas quebra linhas por padrão. Isso é proposital - o RFC 4648 diz que as implementações não devem acrescentar quebras de linha a menos que a especificação ao redor peça explicitamente - e é um alívio, porque uma quebra de linha solta dentro de uma string JSON ou de uma URL é erro, não recurso. A quebra de linha existe para email e PEM, e quando você precisa dele, ou você pega no caminho streaming do OpenSSL, ou você quebra a linha você mesmo em cinco linhas (a seção de email mostra as duas opções). Para escolher uma biblioteca: use OpenSSL se você já linka, Mbed TLS para builds embedded onde cada quilobyte é discutido, APR-Util dentro do ecossistema Apache, e GLib quando o resto do seu programa já é GLib. Instalação: libssl-dev (Debian/Ubuntu) ou openssl-devel (Fedora/RHEL) ou brew install openssl (macOS); libmbedtls-dev para Mbed TLS; libaprutil1-dev mais libapr1-dev para APR-Util; glib2.0-dev para GLib.
Faça a matemática antes de alocar
Antes de qualquer código, a aritmética, porque o C não vai te salvar de um buffer pequeno demais. A cada três bytes de entrada saem exatamente quatro caracteres de saída. Se o comprimento da entrada não for um múltiplo de três, o grupo final ainda produz quatro caracteres e as casas não usadas são marcadas com pads de =: um byte de entrada vira quatro caracteres com dois pads, dois bytes de entrada viram quatro caracteres com um pad. Então o comprimento codificado exato para n bytes é:
size_t encoded_chars(size_t n) {
return ((n + 2) / 3) * 4;
}
Para 1000 bytes isso dá 1336 caracteres; para 1 byte dá 4; para 0 dá 0. Duas correções saem disso. Primeira: OpenSSL e Mbed TLS ambos acrescentam um terminador NUL depois dos dados (e o Mbed TLS reserva espaço para ele quando você pede o tamanho), então o seu buffer quer um byte extra: encoded_chars(n) + 1. Segunda: se você quer saída com linhas quebradas, acrescente uma quebra de linha por linha: o codificador streaming do OpenSSL emite uma linha de 64 caracteres a cada 48 bytes de entrada, então o comprimento quebrado é encoded_chars(n) + (n + 47) / 48. Verifique com 1000 bytes: 1336 caracteres mais 21 quebras de linha dá 1357, e é exatamente isso que o codificador produz. Escreva a fórmula uma vez como função e use em todo lugar; é a diferença entre "cabe" e uma corrupção de heap às 3 da manhã.
size_t b64_buffer_size(size_t in_len) {
return ((in_len + 2) / 3) * 4 + 1; /* caracteres + NUL */
}
size_t b64_buffer_size_wrapped(size_t in_len) {
return ((in_len + 2) / 3) * 4 + (in_len + 47) / 48 + 1;
}
OpenSSL: um bloco só ou uma torneira ligada
A função one-shot do OpenSSL é a mula de carga, e é a mais amigável do grupo:
#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;
}
Ela escreve os caracteres codificados em out, acrescenta um NUL depois deles, e devolve o comprimento sem o NUL - então imprimir com %s é seguro e o comprimento fica disponível se você precisar. O buffer de saída precisa segurar encoded_chars(n) + 1 bytes. Não há caminho de erro para tratar: codificar não pode falhar, porque qualquer byte é entrada legal, e a função não tem nenhum conceito de validação de entrada para tropeçar. O único jeito de dar errado é você dar a ela um buffer pequeno demais, e a seção de matemática é o antídoto.
O par streaming é para quando os dados são grandes ou chegam em pedaços. O EVP_EncodeUpdate processa a entrada em blocos de 48 bytes e escreve 64 caracteres mais uma quebra de linha (65 bytes) por bloco completo, segurando qualquer resto no contexto até chegar mais dados ou a chamada final:
#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 caracteres + 21 quebras de linha + folga */
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;
}
A saída daquele programa são 1357 bytes com 21 quebras de linha - a fórmula da seção de matemática, feita realidade. Duas notas práticas. A nota de versão: desde o OpenSSL 1.1.0 (2016) o tipo de contexto é opaco, então aloque com EVP_ENCODE_CTX_new() e libere com EVP_ENCODE_CTX_free(); o padrão de pilha mais antigo EVP_ENCODE_CTX ctx; que você encontra em tutoriais mirando o 1.0.2 e anteriores não compila contra cabeçalhos modernos, OpenSSL 3.x incluído. E a nota de design: como só blocos completos de 48 bytes saem do EVP_EncodeUpdate, o pipeline em pedaços mais limpo alimenta ele com múltiplos de 48 - então toda linha que a função escreve é uma linha terminada, e o EVP_EncodeFinal sozinho decide como a cauda é quebrada. Se a sua entrada chega em tamanhos arbitrários (uma leitura de rede), o contexto continua cuidando do alinhamento para você; o hábito de múltiplos de 48 é só o que torna a saída previsível.
Mbed TLS: pergunte, depois codifique, receba uma string
O encode do Mbed TLS tem o contrato mais limpo do grupo, construído em volta de uma consulta de tamanho que você pode chamar com um destino 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;
}
Leia os detalhes com cuidado, porque eles são uma masterclass de API amigável. A consulta de tamanho reporta needed como os caracteres codificados mais um para o NUL - para "Mane" isso é 8 mais 1, ou seja, 9 - e ela se anuncia com o código de "buffer muito pequeno" (MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL, que é -0x002A) porque um destino NULL é, por definição, pequeno demais. A chamada de verdade então escreve os caracteres e o NUL, e o *olen volta como 8 - o comprimento sem o terminador - então o buffer já é uma string C imprimível. Se você entregar a ela um buffer um byte curto, você recebe o mesmo código de "pequeno demais" com o tamanho necessário em *olen, então a falha te diz exatamente quanto ficou faltando. Mais uma nota: a biblioteca faz suas consultas de tabela através de helpers de tempo constante, um pequeno toque de cuidado que você não vê na maioria dos codificadores.
APR-Util e GLib: os outros dois
O codificador do APR-Util é um par simples de funções com comprimentos 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;
}
Aqui o apr_base64_encode_len() e o valor de retorno contam o NUL, então o n é um a mais que a contagem de caracteres - uma diferença de contabilidade em relação ao OpenSSL e ao Mbed TLS que já produziu bugs de off-by-one em mais de um codebase. O mesmo limite de comprimento de 32 bits se aplica: para valores perto ou acima de 2 GB, esta não é a ferramenta. Existe também o apr_base64_encode_binary(), que em máquinas EBCDIC pula a conversão de EBCDIC para ASCII da entrada - nos mainframes onde essa conversão aconteceria de qualquer jeito, e uma diferença no-op em todo outro lugar. Esse par, a variante binária, e as funções de decodificação correspondentes são, na verdade, a superfície base64 inteira que o apr-util expõe: não existe variante alocada por pool e não existe variante streaming, então o padrão de malloc acima é o único padrão.
O codificador do GLib é do estilo alocado no heap - você recebe uma string terminada em NUL e um dever:
#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;
}
Sem envolvimento, terminada em NUL, libere com g_free - a anotação G_GNUC_MALLOC no protótipo é o que diz isso aos analisadores estáticos. Quando você quer quebras de linha, o par incremental é a ferramenta: o g_base64_encode_step() recebe um inteiro de estado e uma flag break_lines e te diz quantos bytes de saída ele escreveu, e o g_base64_encode_close() termina o grupo parcial final. É a mesma forma de máquina de estados do par streaming do OpenSSL, só que com o estilo de parâmetros do GLib.
Base64 URL-safe feito à mão
As quatro bibliotecas acima falam todas o alfabeto padrão: A-Z, a-z, 0-9, mais, e barra. A web, porém, fala cada vez mais o segundo dialeto da seção 5 do RFC 4648, chamado base64url: a mesma codificação com + trocado por - e / trocado por _, e o padding de = no final jogado fora quando o comprimento é conhecido. JSON Web Tokens, parâmetros state do OAuth e inúmeros IDs de API usam, porque + e / são ambos perigosos em URLs, enquanto - e _ são caracteres não reservados que passam voando. Como nenhuma biblioteca de C emite este dialeto nativamente, você faz você mesmo - e são duas pequenas mudanças, porque você já tem um codificador de alfabeto padrão:
#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'; /* padding intencionalmente descartado */
}
O uso é dois passos - codifique no padrão, depois traduza:
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 */
Duas cautelas. O laço para no primeiro =, e é isso que joga fora o padding - não "corrija" isso, jogar fora o padding é o objetivo (o receptor que precisar dele pode readicionar a partir do comprimento). E dimensione o out para o comprimento codificado completo, não menos: a tradução é caractere por caractere até os pads, então a capacidade que você já alocou para a forma padrão é exatamente a certa. Uma nota honesta para interoperabilidade: se os seus dados não contiverem bytes que mapeiem para + ou /, as formas padrão e URL-safe são idênticas e nada vai reclamar de uma confusão - o bug só aparece quando os dados finalmente contêm um. Trate o dialeto como propriedade do canal (URLs, tokens), não dos dados.
Texto e charsets: UTF-8 é só bytes
Uma pergunta que surpreende recém-chegados em C: o que acontece com texto acentuado, emoji, caracteres CJK? A resposta é o fato mais libertador deste artigo - nada precisa acontecer. O Base64 opera em bytes, e o C é uma linguagem de bytes. Se o seu texto é UTF-8 (o que, em 2026, ele provavelmente é), a codificação UTF-8 de "café" são cinco bytes - 63 61 66 c3 a9 - e o Base64 codifica esses cinco bytes exatamente como qualquer outro cinco bytes, produzindo Y2Fmw6k=. Sem parâmetro de charset, sem BOM, sem passo de conversão, sem chamada de biblioteca. O codec não sabe nem se importa o que os bytes significam; esse é o design inteiro.
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
const char *utf8 = "caf\303\251"; /* café em 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;
}
Duas armadilhas moram nas bordas desta seção. A primeira é o wchar_t: se os seus dados chegaram como caracteres largos, você precisa primeiro convertê-los para uma sequência de bytes (no Linux, UTF-8, via wcstombs() ou a sua máquina de locales) antes de codificar - Base64 de um array de wchar_t é a codificação de uma representação interna, não do texto, e vai diferir entre plataformas. A segunda é a codificação da fonte: o literal de string no seu arquivo C está codificado na codificação do arquivo fonte (UTF-8 em qualquer projeto moderno), então escrever "café" direto funciona desde que o arquivo realmente seja UTF-8 e o seu compilador seja avisado disso (ele é, por padrão, nas toolchains modernas). Codifique os bytes que você quer enviar, e deixe o receptor lidar com o que os bytes significam.
Imagens: de um buffer para uma string
O trabalho "de verdade" mais comum de codificação em C web: um arquivo binário - um JPEG, um PNG, um ícone - precisa viajar por um canal de texto, então vira uma string Base64. A receita é ler o arquivo num buffer, dimensionar a saída com a fórmula, codificar, e seguir em frente. A metade de leitura de arquivo merece cuidado, porque é onde programas C realmente quebram:
#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;
}
Notas: rb para ler binário - inegociável em qualquer plataforma, porque o modo texto pode traduzir bytes e mudar o got; o par fseek/ftell para dimensionar (para pipes e sockets sem seek, leia num buffer que cresce no lugar); e got em vez de size na codificação, porque uma leitura curta é uma possibilidade real. Uma imagem de 1 MB vira mais ou menos 1.33 MB de texto - esse é o imposto, cobrado adiantado, e é por isso que um payload de imagem base64-em-JSON deveria te fazer pausar e perguntar se um upload de arquivo de verdade teria sido mais barato.
Arquivos e o hábito .b64
A outra face do trabalho de imagem: você precisa escrever a forma Base64 de um arquivo no disco - um sidecar .b64, um backup de um binário num armazenamento seguro para texto, um anexo para um mailer. A mesma matemática, um escritor diferente. O hábito que vale adotar é escrever a saída de texto com quebras de linha explícitas num comprimento que o lado receptor espera - 76 para email, 64 para consumidores estilo PEM, ou nenhuma se o receptor for o seu próprio código:
#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;
}
O laço escreve linhas de 76 caracteres e uma linha final mais curta; um decodificador que pula espaços em branco (todo sério faz) não vai se importar com o comprimento da linha de jeito nenhum, por isso o receptor é quem se consulta, não o seu gosto. Mantenha o arquivo de saída em modo texto (w) no lado da escrita se você quer quebras de linha da plataforma, ou wb se o receptor conta caracteres rigorosamente - e se ele conta, ele quer exatamente o que você prometeu: 76 caracteres mais uma quebra de linha, nada mais. Essa promessa, não os bytes, é o que transforma um arquivo .b64 num formato.
Data URIs: incorporação de verdade
Data URIs (RFC 2397) são o outro lado da chegada favorita do artigo de decodificação: em vez de receber data:image/png;base64,..., você constrói um. A forma é data:, o media type, ;base64, uma vírgula, o payload - e construí-lo em C é um snprintf depois do encode:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
/* a assinatura PNG de 8 bytes */
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;
}
Três notas de design. O media type que você coloca no URI é uma alegação pela qual você é responsável - fareje os magic bytes do arquivo real primeiro, ou um data:image/png carregando um JPEG vai confundir todo consumidor de um jeito diferente. A flag ;base64 é obrigatória quando o payload é Base64; omita-a e o payload deve ser texto percent-encoded no lugar, o que é um formato inteiramente diferente. E a própria orientação do RFC é que data URIs são para valores curtos: incorporar um logo de 5 MB inline numa página HTML funciona, mas é um cheiro de design que uma URL de asset de verdade corrigiria. A mesma construção aparece o tempo todo em APIs JSON onde um cliente quer um avatar no mesmo pedido que os dados do formulário - codifique, anteceda, envie.
HTTP e JSON: payloads que sobrevivem
O maior motivo moderno para codificar em C é o JSON. Uma string JSON é uma sequência de caracteres com regras de escape, e bytes crus não cabem: um NUL no meio de um literal de string é um problema de C, uma quebra de linha literal dentro de uma string JSON é JSON inválido, e bytes arbitrários precisam de uma história de escape definida. O Base64 desvia do problema inteiro produzindo apenas caracteres que o JSON nunca precisa escapar - os 64 caracteres do alfabeto mais, no dialeto padrão, o =, nenhum dos quais são aspas ou backslashes. O binário entra como string e sai do outro lado exatamente como entrou:
#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;
}
Isso imprime {"avatar": "aGVsbG8="} - um objeto JSON completo e válido, sem mecanismo de escape envolvido, e a precisão do %.*s mantém o comprimento exato mesmo se um dia você trocar para um codificador que não termina em NUL. O custo honesto é o tamanho: cada byte que você envia como string JSON custa 4/3 de byte de ar mais o nome do campo e as aspas, então um binário de 10 KB vira uma string de 13.3 KB dentro do JSON. Para blobs pequenos ocasionais (ícones, thumbnails, assinaturas, tokens) esse é um preço justo; para um upload de 500 MB é uma arquitetura que você vai se arrepender, e um upload de arquivo de verdade é a ferramenta para aquele trabalho. Vale uma linha também: os + e / do alfabeto padrão são seguros dentro de uma string JSON, mas se a mesma string depois viajar numa query de URL, eles não são - esse é o trabalho da seção de URL-safe.
JWTs: três partes, um alfabeto
O consumidor de destaque do base64url em C é o JSON Web Token. Um JWT compacto conforme o RFC 7519 é três partes codificadas em base64url unidas por pontos - header, payload, assinatura - e construir um é um exercício agradável porque cada peça é uma função que você já tem: codifique no padrão, traduza para URL-safe, assine, repita. Aqui está um token HS256 construído com o HMAC do OpenSSL:
#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;
}
Duas coisas que a estrutura ensina. Primeira: a entrada de assinatura é as duas partes URL-safe unidas por um ponto - exatamente os bytes que o receptor vai ver - então a tradução para base64url tem que acontecer antes de assinar, não depois; assine a forma de alfabeto padrão e a verificação do receptor falha, o que é um bug que compila, roda, e parece um descompasso de chave. Segunda: o header e o payload são JSON puro num invólucro Base64: qualquer pessoa pode lê-los, e esse é o design. Um token é uma nota assinada, não um envelope lacrado - então não coloque nada neles que você se importasse se um usuário interceptador lesse, e nunca, jamais, coloque uma senha num payload de JWT "porque está codificado". A parte Base64 deste trabalho é pequena e chata, o que é o maior elogio que você pode fazer a uma implementação de JWT.
Basic Auth HTTP: construindo o token
O header de autenticação mais antigo é também o trabalho Base64 mais simples: username:password, codificado no alfabeto padrão, depois da palavra Basic. Construí-lo em C são duas linhas, e a única sutileza é que a senha pode conter um dois-pontos (e a separação no lado receptor tem que ser no primeiro):
#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;
}
Isso imprime Authorization: Basic YWxpY2U6czNjcjN0. O aviso do RFC se aplica também no lado do envio: isso é codificação, não proteção. Por uma conexão HTTP pura, a credencial está a um base64 -d de distância de qualquer pessoa no fio, então Basic auth é um hábito exclusivo de HTTPS. (As alternativas modernas - bearer tokens, mTLS - todas reaproveitam a mesma mecânica: monte uma string, codifique, coloque num header. O Base64 tem sido o jeito do HTTP de contrabandear dados estruturados por headers de texto desde que o protocolo tinha headers.)
Email e PEM: onde a quebra de linha mora
O email é o motivo pelo qual a quebra de linhas existe. O SMTP limita o comprimento da linha, então o MIME tetou as linhas codificadas em 76 caracteres (o PEM, seu ancestral, em 64), e todo sistema de email respeitou esse teto por trinta anos. Se o seu programa C produz Base64 para um corpo de email ou um anexo, a quebra de linha não é cosmética opcional - uma linha de 200 KB sem quebra de linha vai ser rejeitada ou estragada por partes da infraestrutura de email. O codificador streaming do OpenSSL te dá saída com quebra de linha de graça (no seu comprimento hereditário de 64 caracteres), e quando você precisa de exatamente 76, quebrar um resultado one-shot é um laço de cinco linhas:
#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;
}
Note o \r\n: o email quer quebras de linha CRLF, e se o texto codificado é uma de várias partes MIME, tudo ao redor do bloco base64 segue as mesmas regras - comprimentos de linha, finais CRLF, sem exceções. Arquivos PEM (o formato da maioria das chaves e certificados) usam a mesma ideia com linhas de 64 caracteres entre marcadores -----BEGIN e -----END, e as ferramentas do OpenSSL esperam ver aquela armadura quando você re-salva uma chave - então se o seu programa toca em PEM, quebre em 64 e mantenha os rótulos. Em todo outro lugar - JSON, URLs, APIs, bancos de dados - a regra do RFC se aplica e você não quebra linhas de jeito nenhum.
Contrabandear valores por configs e colunas
O caso de uso discreto: valores que quebrariam um formato de texto são embalados como Base64 para não quebrarem. Um DSN de banco com ponto-e-vírgulas, uma senha com aspas, um token com quebra de linha - o cara de ops codifica uma vez e o arquivo de configuração nunca vê os caracteres problemáticos:
#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;
}
O programa imprime a linha exata para colar num arquivo .env, e o código C que depois lê é um getenv mais um decode. Três ressalvas honestas, todas sobre o que isso não é. Não é criptografia: qualquer pessoa que consiga ler o arquivo de configuração decodifica o valor numa chamada, então nunca embale um segredo como Base64 e chame de protegido. Não é escape: se o formato precisa de estrutura preservada, uma codificação de verdade (percent-encoding para URLs, escape de JSON para JSON) é a ferramenta certa, e o Base64 é para os valores que esses formatos não conseguem expressar - os binários. E custa tamanho: um valor guardado numa coluna TEXT de banco como Base64 ocupa mais ou menos 33 por cento mais espaço que o original, o que é ok para tokens e um número de verdade para colunas de arquivo (é para isso que existem colunas BLOB).
Codificando da shell
Antes de correr para uma chamada do cc, lembre que as duas ferramentas padrão codificam, e rápido. O coreutils é o instrumento geral: base64 codifica com quebra de linha de 76 caracteres por padrão, -w muda a coluna, e -w 0 desliga a quebra de linha completamente:
base64 photo.png > photo.b64
base64 -w 0 photo.png > photo-oneline.b64
cat note.txt | base64 -w 0
A ferramenta do OpenSSL é o mesmo trabalho com embalagem de linhagem TLS: openssl base64 (o alias amigável de openssl enc -base64) quebra em 64 caracteres e -A troca para uma linha única:
openssl base64 < photo.png > photo.b64
openssl base64 -A < photo.png > photo-oneline.b64
Por que se importar com a diferença de quebra de linha? Porque as saídas padrão das duas ferramentas não são intercambiáveis se um parser a jusante conta caracteres - 76 por linha versus 64 por linha é uma diferença visível no arquivo, e um parser que corta espaços em branco não se importa enquanto um que valida o comprimento da linha se importa de vez. Quando o seu programa C é o produtor e a shell o consumidor (ou vice-versa), combinem a quebra de linha primeiro. Uma nota de dialeto para sistemas com tempero BSD: a flag de decodificação lá historicamente era -D, e versões antigas do macOS ainda se lembram dela; o lado da codificação é base64 em todo lugar, o que é a única direção da qual esta seção se importa de qualquer jeito.
Streaming das coisas grandes
Codificar um arquivo de vários gigabytes num único malloc é um problema de memória que você não precisava ter. O caminho streaming existe exatamente para isso, e a disciplina de blocos do OpenSSL torna o código quase trivial: alimente o EVP_EncodeUpdate com o quanto o arquivo te der, deixe ele segurar o resto de cada bloco parcial de 48 bytes no contexto, e escreva os 65 bytes de saída de cada bloco direto no arquivo de destino. A memória de pico são os seus dois buffers - algumas dezenas de quilobytes - não importa o quão grande o arquivo seja:
#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]; /* um múltiplo de 48: linhas limpas */
unsigned char outbuf[1024 * 65 + 65]; /* 65 bytes de saída por bloco de 48 bytes, mais folga */
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;
}
Dois detalhes de design estão fazendo trabalho naquele laço. O buffer de entrada é um múltiplo de 48 bytes, então cada chamada entrega blocos completos ao codificador e toda linha que ele escreve é uma linha de 64 caracteres terminada; a chamada final então quebra a cauda de verdade. Se a sua entrada vem em tamanhos arbitrários (um socket, um disco lento), o contexto continua absorvendo o desalinhamento corretamente - a escolha do múltiplo de 48 é sobre previsibilidade da saída, não sobre correção. O segundo detalhe é o tamanho do buffer de saída: 65 bytes a cada 48 bytes de entrada mais a folga do bloco final (66), o que é a fórmula quebrada da seção de matemática aplicada por pedaço. Relatório de progresso é uma linha - conte os bytes escritos em out contra o total que você calculou do tamanho do arquivo - e como codificar faz os dados crescerem, o arquivo de saída vai pousar mais ou menos 33 por cento maior que a entrada: cobre o disco adiantado.
As arestas afiadas da codificação
As armadilhas, reunidas, todas com formato de C:
- Off by one, nas duas direções. O OpenSSL devolve o comprimento sem o NUL dele; a consulta de tamanho do Mbed TLS inclui espaço para o NUL; o APR conta o NUL nos comprimentos. Três bibliotecas, três convenções de contabilidade. Escreva a função de tamanho de buffer uma vez (a seção de matemática) e pare de fazer aritmética na cabeça no ponto de chamada.
- O NUL é um byte que você tem que pagar. Todo codificador deste artigo quer um byte extra na saída para o terminador, e o mesmo vale para qualquer concatenação de buffer que você escrever à mão. Um buffer dimensionado exatamente em
encoded_chars(n)fica um byte curto no momento em que alguém quer queprintf("%s")funcione. - Codificar não pode falhar, então você não pode deixar estourar. Não existe código de erro que pegue um buffer pequeno demais - o codificador vai escrever alegremente além do fim. O modelo de falha da codificação Base64 em C é inteiramente seu: dimensione direito, ou ele corrompe memória sem nenhum diagnóstico.
- Encoding duplo é o bug silencioso clássico. Um valor que já é Base64, passado de novo pelo codificador, produz uma string Base64 perfeitamente válida que decodifica para uma string Base64 em vez dos dados. O sintoma - "decodifica, mas para a coisa errada" - leva uma tarde para encontrar. Se um valor chega "pré-codificado", verifique que o comprimento dele é um múltiplo de quatro e que contém só caracteres do alfabeto antes de assumir que é dado cru; se estiver codificado, pule o encode.
- O sinal de mais em URLs. Saída em alfabeto padrão colocada numa query string chega com o
+transformado em espaço até o seu servidor fazer o parse do formulário - o+é um espaço em percent/form encoding. Tokens e IDs que viajam em URLs querem o dialeto URL-safe, ponto final. - Modo texto do lado errado do cano. Ler um arquivo binário em modo texto pode traduzir bytes (em algumas plataformas) e mudar o seu comprimento; escrever Base64 quebrado com a convenção errada de quebra de linha quebra receptores que contam caracteres.
rbpara entrada binária,\r\nou\nexplícito onde uma especificação exige, e nunca deixe o runtime do C decidir suas quebras de linha em silêncio. - Estouro de int na matemática de tamanho.
((n + 2) / 3) * 4em aritmética deintestoura para entradas acima de mais ou menos 1.5 GB, produzindo um "tamanho necessário" pequeno e positivo e um amassado de heap. Faça a matemática emsize_t(ouuint64_t), o que também explica por que a API baseada em int do APR tem um teto de 2 GB que você não consegue burlar com engenharia. - Quebra de linha onde o receptor não espera. O RFC 4648 diz: sem quebras de linha a menos que a especificação ao redor peça. Uma quebra de linha dentro de um valor de string JSON é inválida; dentro de uma URL é um pedido diferente. Quebre para email, quebre para PEM, e em lugar nenhum outro.
A checklist curta
Calcule o tamanho do buffer com a fórmula, não com um chute, e mantenha uma única função de tamanho para o codebase inteiro. Mantenha (ponteiro, comprimento) juntos mesmo quando o buffer é terminado em NUL, porque o comprimento é o contrato e o NUL é uma conveniência. Escolha o dialeto pelo canal: padrão para JSON e corpos, URL-safe para URLs e tokens, quebrado para email e armadura, sem quebra de linha em todo outro lugar. Verifique os magic bytes antes de alegar um tipo MIME num data URI. Nunca use Base64 como criptografia, como substituto de percent-encoding, ou como lugar para esconder um segredo - ele é uma caixa, não uma tranca. E quando os dados são grandes, faça streaming: os codificadores baseados em blocos foram desenhados exatamente para isso, e memória constante é o ponto inteiro.
História: como a embalagem foi padronizada
A história do codificador é a história dos comprimentos de linha. O primeiro Base64 era um programa C dos início dos anos 1990. O Privacy-Enhanced Mail (RFC 1421, 1993) precisava carregar binário pelo email de 7 bits, e os autores dele escolheram seis bits por caractere em linhas de 64 caracteres - o 64 é um fósil da tolerância de comprimento de linha do SMTP, e o código C fazia a embalagem consulta de tabela após consulta de tabela. Quando o MIME padronizou o mesmo alfabeto para a web (RFC 1521 em 1993, RFC 2045 em 1996), ele relaxou a linha para 76 caracteres, e o mundo carregou dois hábitos - 64 e 76 - que ambos alegavam ser "o" comprimento de linha do Base64. Os codificadores corresponderam: o caminho streaming do OpenSSL manteve 64 (a herança do PEM), a ferramenta do coreutils escolheu 76 (a herança do MIME), e as duas ferramentas na mesma máquina ainda não concordam sobre onde as quebras de linha vão. O padrão finalmente tomou posição em 2006: o RFC 4648 disse que as implementações não devem acrescentar quebras de linha de jeito nenhum a menos que a especificação que as referencia instrua explicitamente, por isso toda biblioteca deste artigo tem saída sem quebra de linha por padrão, e por isso a quebra de linha agora é um recurso opt-in para email e armadura. O próprio alfabeto, as regras de padding, e a regra de canonicidade de "os bits de pad devem ser zero" datam daqueles RFCs anteriores de PEM e MIME, e o 4648 os repete como as regras canônicas da família. E a seção 11 do RFC aponta para uma implementação de referência - um programa ISO C99, hospedado externamente porque o código em si "não pôde ser incluído neste RFC por razões procedurais" - outro lembrete de que, neste formato, o C não é um cidadão de segunda categoria. A biblioteca padrão do C, por sua vez, nunca se recuperou: o C89 congelou em 1990, antes de qualquer uma dessas coisas existir, e o C23 em 2024 ainda sai sem uma função Base64. Então as bibliotecas que você linka são o padrão, e a escolha entre elas é uma decisão de design pequena mas real - é o que este artigo tem tratado.
Fatos pequenos e estranhos
Alguns fatos que são simplesmente divertidos, todos sobre o lado da embalagem em C:
- O "64" é a base: cada caractere de saída são seis bits, e 2 elevado a 6 é 64. O formato nomeia o alfabeto dele do jeito que o C nomeia os inteiros - pelo que o número realmente é.
- O bloco de 48 bytes do codificador streaming do OpenSSL não é contabilidade arbitrária: 48 bytes de entrada são exatamente 16 grupos de 3, e 64 caracteres de saída são exatamente 16 grupos de 4. Os dois números são múltiplos de 16, o que é o tipo de redondeza que deixa hardware e linhas de cache felizes - ou ao menos deixa felizes os humanos que leem o código.
- Um byte de entrada codifica em quatro caracteres, dois dos quais são
=. O payload não vazio mais pequeno possível é 50 por cento padding - a codificação mais desperdiçadora do formato, e a que todo suite de testes usa porque é tão fácil escrever errado. - O Mbed TLS é o único codificador deste artigo que faz suas consultas em tempo constante, porque as pessoas que escrevem criptografia embedded não confiam em indexação de tabela de tempo variável nem num codec que não é um cifrador. A paranoia transfere.
- A regra de codificação canônica - os bits de pad não usados devem ser zero - soa trivial até você descobrir que violá-la significa que duas strings diferentes podem decodificar para os mesmos bytes, o que quebra toda verificação de "essa string é a codificação daquele arquivo?" que existe. Os seus codificadores todos cumprem; é por isso que o base64 é uma representação estável por hash e pode fazer as vezes de nome de arquivo num content store.
- O
EVP_EncodeBlockdo OpenSSL é uma das raras funções de C cujo valor de retorno, a própria saída, e o terminador NUL todos concordam: ele escrevencaracteres, um NUL, e devolven. Numa linguagem famosa por off-by-one, esse é um momento de paz. - O APR-Util é o único codificador aqui que pergunta o que EBCDIC significa, porque o Apache ainda roda em máquinas onde as letras estão numa ordem diferente da do ASCII. Nessas máquinas, "codificar" uma string inclui primeiro reordenar em silêncio o alfabeto dela.
- A entrada vazia codifica para a string vazia em toda biblioteca, sem pads e sem quebras de linha. O elemento identidade do formato, presente e correto nas quatro, o que o torna o teste unitário mais barato que você vai escrever.
Virando para o lado do decodificador
Então essa era a parte da embalagem: a matemática, os quatro codificadores, os dialetos, e os lugares para onde os bytes vão. É a metade calma do trabalho, porque codificar não tem entrada inválida e não tem decodificador para discordar de você. A outra direção - encontrar o Base64 do mundo de fora e recuperar os bytes - é onde a dor se concentra: caudas com zero-padding, truncamento silencioso, alfabetos estritos versus tolerantes, e uma linha de comando que come quebras de linha finais. A decodificação Base64 em C é coberta em profundidade no artigo relacionado, linkado a partir desta página, e ele é o companheiro natural deste: o codificador escreve a caixa, o decodificador abre, e entre os dois você tem todo trabalho Base64 que um programa C vai encontrar.
Última atualização: 2026-09-08
Artigo relacionado: Decodificação Base64 em C: um guia completo