Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Decodificação Base64 em C: um guia completo

Ela mora numa resposta de API, num arquivo de configuração, num anexo de email ou no meio de uma URL: uma string longa de letras e dígitos, com o ocasional + ou /, e talvez um ou dois = no final. Você a reconhece na hora, e agora precisa dos bytes originais de volta - em C. Esse é o trabalho inteiro da decodificação Base64: quatro caracteres do alfabeto entram, três bytes crus saem, uma vez atrás da outra, até as marcas de = dizerem onde os dados de verdade terminaram. A página inicial deste site passeia pelo formato passo a passo, então este artigo gasta a energia onde o trabalho de verdade mora: nos buffers, nas bibliotecas e nas armadilhas que vivem entre eles.

Duas coisas para saber antes do primeiro malloc. Primeira: decodificar é a direção que encolhe: a saída tem três quartos do tamanho da entrada, então um decodificador nunca precisa de mais memória do que o payload que já segura. Segunda - e essa é a manchete - o C não vem com um decodificador Base64. A biblioteca padrão da linguagem congelou muito antes de o Base64 existir, e nenhum padrão desde então preencheu o vazio. Então todo programa em C que decodifica Base64 depende de uma biblioteca, e as quatro que importam na prática são OpenSSL, Mbed TLS, APR-Util e GLib. Cada uma tem uma personalidade diferente: o que ela perdoa, como ela reporta erros e o que ela faz em silêncio com a sua saída. Uma vez que você conhece a personalidade do seu decodificador, decodificar Base64 em C deixa de ser fonte de bugs misteriosos e vira uma rotina que você escreve dormindo.

A caixa de ferramentas: quatro jeitos de recuperar seus bytes

Aqui está o cenário de relance. As quatro cobrem o alfabeto padrão; as diferenças estão nas bordas, e é das bordas que os bugs vêm.

Biblioteca Cabeçalho Modelo de erro Mania de saída para lembrar
OpenSSL (libcrypto) <openssl/evp.h> Retorna -1 em entrada ruim O decodificador one-shot faz zero-padding na cauda
Mbed TLS <mbedtls/base64.h> Códigos de retorno (-0x002C, -0x002A) As regras de entrada mais estritas das quatro
APR-Util <apr-1.0/apr_base64.h> Nenhum: para no primeiro caractere estranho API com comprimento int, então 2 GB é o teto
GLib <glib.h> Retorna NULL só em falha dura Ignora em silêncio o lixo solto

A instalação é um nome de pacote por distro. Para OpenSSL: libssl-dev no Debian e no Ubuntu, openssl-devel no Fedora e no RHEL, openssl no Arch, e brew install openssl no macOS. Para Mbed TLS: libmbedtls-dev (ou mbedtls). Para APR-Util: libaprutil1-dev mais libapr1-dev. Para GLib: glib2.0-dev. Depois você linka com -lcrypto, -lmbedcrypto, -laprutil-1 ou -lglib-2.0, respectivamente. Qual você escolhe? Se você já linka OpenSSL para TLS ou hash (a maioria dos servidores faz), use OpenSSL. Para builds embedded e com recursos limitados, o Mbed TLS é o cidadão pequeno e estrito. Se você está dentro do ecossistema Apache, o APR-Util já está lá. Se o seu codebase é baseado em GNOME ou GTK, o GLib mantém tudo em um runtime só.

OpenSSL: o decodificador que enche as lacunas com zeros

O OpenSSL carrega o Base64 em dois sabores. A função one-shot é a estrela da maioria do código:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  unsigned char out[16];
  const char *payload = "TWFuZQ==";
  int n = EVP_DecodeBlock(out, (const unsigned char *)payload,
                          (int)strlen(payload));
  if (n < 0) {
    printf("not base64\n");
    return 1;
  }
  printf("%d bytes\n", n);
  return 0;
}

Dê a ela um buffer de caracteres Base64 e um comprimento, e ela escreve os bytes decodificados em out e devolve a quantos deram. Ela corta espaços em branco no início, corta espaços em branco e quebras de linha no final, e se recusa a aceitar entrada que não seja um múltiplo de quatro caracteres após o corte, ou que contenha um caractere fora do alfabeto. Até aqui, um contrato perfeitamente sensato. Exceto por um detalhe que já corrompeu, em silêncio, mais de uma importação de banco de dados: o valor de retorno não é o comprimento real dos dados.

Rode aquele programa e você vai obter 4 bytes... não, espera. TWFuZQ== são dois grupos de quatro caracteres, então a função devolve 6, e o buffer guarda 4d 61 6e 65 00 00: a palavra "Mane" mais dois bytes de zero. O decodificador one-shot do OpenSSL trabalha em quanta fixos - a cada quatro caracteres de entrada saem exatamente três bytes de saída - e quando o último grupo carregou só um byte de verdade, as outras duas casas são preenchidas com zeros. O manual menciona isso numa única frase calma ("a saída será preenchida com bits de zero, se necessário"), e essa frase única é a mais importante de toda a man page desta função.

O comprimento real é recuperado do padding, e é um cálculo de duas linhas:

size_t real_length(const char *b64) {
  size_t len = strlen(b64);
  while (len > 0 && b64[len - 1] == '=') len--;
  return len * 3 / 4;
}

Conte os caracteres do alfabeto, jogue fora os pads do final, multiplique por três, divida por quatro. Para TQ== (a letra M, codificada), isso dá (2 * 3) / 4 = 1 byte de verdade - enquanto o EVP_DecodeBlock vai reportar três. Mantenha sempre o par (ponteiro, comprimento) junto, e nunca use strlen em dados decodificados, porque os bytes que você recebeu podem ser um JPEG e o primeiro deles pode ser um NUL.

O decodificador streaming: um decodificador que sabe quando parar

Para tudo o mais, o OpenSSL oferece o par streaming EVP_DecodeUpdate mais EVP_DecodeFinal. O objeto de contexto é o que move o estado entre as chamadas: ele segura de um a três caracteres de um grupo inacabado para você poder alimentar o payload em pedaços. O comportamento que importa é este: espaços em branco (espaços, tabs, carriage returns, line feeds) são pulados em qualquer lugar do stream, qualquer outro caractere fora do alfabeto ou um = no meio dos dados devolvem -1 na hora, e um retorno de 0 de um update significa "o padding foi visto, nada mais é esperado". O EVP_DecodeFinal então se recusa com -1 se um grupo parcial ainda estiver pendente, porque um comprimento que não é múltiplo de quatro (após os espaços em branco) não é um payload válido.

Um aviso de versão antes do código, porque tutoriais antigos vão te fazer tropeçar: no OpenSSL 3.x o tipo de contexto EVP_ENCODE_CTX é opaco, então o padrão de pilha EVP_ENCODE_CTX ctx; encontrado em muito código da internet não compila mais. Aloque e libere explicitamente:

static int decode_b64(const unsigned char *in, int in_len,
                      unsigned char *out, int *out_len) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  if (ctx == NULL) {
    return -1;
  }
  *out_len = 0;
  EVP_DecodeInit(ctx);
  int r = EVP_DecodeUpdate(ctx, out, out_len, in, in_len);
  if (r < 0) {
    EVP_ENCODE_CTX_free(ctx);
    return -1;
  }
  int tail = 0;
  r = EVP_DecodeFinal(ctx, out + *out_len, &tail);
  EVP_ENCODE_CTX_free(ctx);
  if (r < 0) {
    return -1;
  }
  *out_len += tail;
  return 0;
}

Dimensione o buffer de saída em in_len * 3 / 4 + 3 e a chamada fica segura para qualquer entrada. Veja ele lidando com um payload quebrado em MIME em que a quebra de linha cai no meio de um grupo:

const char *wrapped = "TWFu\nZQ==";
unsigned char out[16];
int out_len = 0;
if (decode_b64((const unsigned char *)wrapped,
    (int)strlen(wrapped), out, &out_len) != 0) {
  printf("invalid base64\n");
  return 1;
}
printf("%.*s\n", out_len, out); /* Mane */

A quebra desaparece, os quatro bytes saem, e ninguém precisou pré-limpar a entrada. Há uma diferença bônus em relação à função one-shot: o caminho streaming conta os bytes com honestidade. Dê a ele TQ== e ele devolve exatamente um byte (4d), sem zero-padding nenhum, porque entende que dois pads significam que duas das três casas de saída nunca foram preenchidas. Se o seu payload precisar algum dia de um comprimento confiável do OpenSSL, este é o caminho a usar.

Mbed TLS: o estrito

O Mbed TLS (a biblioteca de criptografia que começou a vida como PolarSSL e agora vem embutida nas pilhas embedded da ARM) te dá duas funções com um contrato muito limpo:

int mbedtls_base64_encode(unsigned char *dst, size_t dlen, size_t *olen,
                          const unsigned char *src, size_t slen);
int mbedtls_base64_decode(unsigned char *dst, size_t dlen, size_t *olen,
                          const unsigned char *src, size_t slen);

Decodifica como uma pessoa cuidadosa faria. Chame com dst definido como NULL (ou dlen como zero) e ela te diz o tamanho necessário em *olen sem fazer nenhum trabalho; chame de verdade e você recebe 0 em caso de sucesso, MBEDTLS_ERR_BASE64_INVALID_CHARACTER (que é -0x002C) se algo na entrada estiver errado, ou MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL (que é -0x002A) se o destino for pequeno demais. O comprimento decodificado cai em *olen, e ao contrário da função one-shot do OpenSSL, ele é sempre o número honesto: decodificar TQ== te dá um byte, 4d, nada mais.

As regras de entrada são as mais estritas das quatro bibliotecas, e valem a pena memorizar porque definem o que "válido" significa para o Mbed TLS:

  • Quebras de linha CRLF e LF podem aparecer entre grupos - payloads de email funcionam como estão.
  • Espaços são permitidos logo antes de uma quebra de linha e no final do buffer, mas um espaço depois de uma quebra de linha ou no meio de um grupo é erro.
  • No máximo dois caracteres =, e só no final; qualquer dado depois de um pad é erro.
  • Qualquer byte acima de 127 (acentos, fragmentos UTF-8, lixo binário) é erro.

Foi a última regra que morde: se um payload chega de uma fonte que estragou a codificação de caracteres, o Mbed TLS vai rejeitá-lo onde um decodificador mais preguiçoso teria decodificado dando de ombros. Para qualquer coisa que toque em entrada não confiável, estrito é um recurso. Uma decodificação completa fica assim:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/base64.h>
int main(void) {
  const char *payload = "TWFuZQ==";
  size_t need = 0;
  int rc = mbedtls_base64_decode(NULL, 0, &need,
      (const unsigned char *)payload,
      strlen(payload));
  if (rc != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
    printf("size query failed: %d\n", rc);
    return 1;
  }
  unsigned char *out = malloc(need);
  size_t olen = 0;
  rc = mbedtls_base64_decode(out, need, &olen,
      (const unsigned char *)payload,
      strlen(payload));
  if (rc != 0) {
    printf("decode failed: %d\n", rc);
    free(out);
    return 1;
  }
  printf("%.*s\n", (int)olen, out);
  free(out);
  return 0;
}

(O fato de a consulta de tamanho devolver o código de "muito pequeno" é por design: é assim que a função reporta o que ela teria escrito. Os dois códigos de retorno acima vêm de <mbedtls/base64.h>, o mesmo cabeçalho em que a função é declarada.)

APR-Util e GLib: mais duas cadeiras

O APR-Util - a biblioteca de utilitários do Apache Portable Runtime, a base sobre a qual o Apache HTTP Server é construído - carrega o Base64 desde que o servidor precisa decodificar headers de autenticação Basic. A API é uma pequena família de funções baseadas em int:

#include <apr-1.0/apr_base64.h>
int apr_base64_encode_len(int len);
int apr_base64_encode(char *coded_dst, const char *plain_src,
                      int len_plain_src);
int apr_base64_decode_len(const char *coded_src);
int apr_base64_decode(char *plain_dst, const char *coded_src);

Duas coisas para saber antes de pegar nele. Primeira: os comprimentos são int: de 32 bits, então o teto prático é 2 GB por chamada, o que é ótimo para headers e valores de configuração e péssimo para decodificar um arquivo de 4 GB. Segunda - e essa é a grande - a função de decodificação não tem nenhum retorno de erro. O comportamento só é visível na implementação, não no cabeçalho: o decodificador trata qualquer caractere inválido, incluindo espaços em branco e NUL, como um terminal. Ele decodifica até a primeira coisa que não reconhece, devolve até onde chegou, e não diz nada. Um payload truncado, uma colagem com um comentário no final, um byte corrompido no meio - tudo isso produz uma saída silenciosamente curta. Se você usar o decodificador do APR, precisa comparar o comprimento retornado com o que o payload prometeu; a função não vai fazer isso por você. Não existe um wrapper que aloque por pool - você fornece o buffer de destino, então em código movido a pools você aloca o plain_dst do pool você mesmo. Há também um ângulo EBCDIC que você não encontra em nenhum outro lugar deste artigo: em máquinas EBCDIC as funções convertem a entrada para ASCII antes de codificar e de volta depois de decodificar, então o mesmo código roda nos mainframes que ainda rodam o httpd.

O GLib, o runtime por trás do GTK e da maioria dos aplicativos GNOME, adota a personalidade oposta. Seu decodificador aceita uma string e sempre devolve um buffer recém-alocado (NULL só se você passar um ponteiro NULL), decodificando o que consegue e pulando em silêncio o resto:

#include <glib.h>
gsize out_len = 0;
guchar *bytes = g_base64_decode(payload, &out_len);
if (bytes == NULL) {
  printf("not base64\n");
} else {
  printf("%u bytes\n", (unsigned)out_len);
  g_free(bytes);
}

A armadilha está na palavra "sempre". O decodificador do GLib é da escola tolerante: caracteres fora do alfabeto são pulados, não fatais. Dê a ele TWFuZ@== e ele devolve os três bytes de "Man" sem erguer um dedo. Existe também uma variante in-place conveniente, o g_base64_decode_inplace(), que decodifica por cima do buffer de entrada (seguro porque a saída é menor que a entrada) e devolve o mesmo ponteiro, então o resultado começa no começo do buffer - um bom truque para código apertado em memória, e ele engole com prazer entrada com quebra de linha em CRLF. O recado para desenvolvedores de C: se os seus dados são não confiáveis, o GLib não vai te salvar de um payload corrompido. As variantes _step (o g_base64_decode_step com um inteiro de estado) estão disponíveis quando você precisa de decodificação incremental, e o par correspondente g_base64_encode_step/g_base64_encode_close mora do lado da codificação.

Base64 URL-safe: o outro alfabeto

Em algum lugar entre o alfabeto padrão e as suas URLs, alguém se feriu. O Base64 padrão usa + e / como seus dois símbolos mais altos, e os dois são problema em URLs: um + numa query string é rotineiramente interpretado como espaço até o seu servidor ver, e / é um separador de caminho. O RFC 4648, seção 5, define a correção, chamada base64url: a mesma codificação com + substituído por -, / substituído por _, e o padding de = no final jogado fora quando o comprimento é conhecido de outro jeito. JSON Web Tokens, parâmetros state do OAuth e uma grande quantidade de IDs de sessão de API vivem neste dialeto.

Nenhuma das quatro bibliotecas de C decodifica base64url nativamente, então a conversão é um helper pequeno que você escreve uma vez e reusa: mapeie os dois caracteres especiais de volta, readicione qualquer padding faltando, e depois entregue o resultado ao seu decodificador padrão. Verificação de comprimento primeiro, porque um comprimento que é um a mais que um múltiplo de quatro é impossível em qualquer dialeto de Base64:

int base64url_decode(const char *url_safe, unsigned char *out,
    size_t out_cap, size_t *out_len) {
  size_t len = strlen(url_safe);
  if (len % 4 == 1) {
    return -1;
  }
  size_t needed = (len * 3) / 4;
  if (needed > out_cap) {
    return -2;
  }
  char *std = malloc(len + 4);
  if (std == NULL) {
    return -3;
  }
  for (size_t i = 0; i < len; i++) {
    char c = url_safe[i];
    if (c == '-') c = '+';
    if (c == '_') c = '/';
    std[i] = c;
  }
  size_t pad = (4 - len % 4) % 4;
  for (size_t i = 0; i < pad; i++) {
    std[len + i] = '=';
  }
  int n = EVP_DecodeBlock(out, (const unsigned char *)std,
                          (int)(len + pad));
  free(std);
  if (n < 0) {
    return -1;
  }
  *out_len = needed;
  return 0;
}

Duas armadilhas guardam este caminho. A primeira é a direção: se você alimentar um payload URL-safe no decodificador padrão sem a troca de caracteres, o OpenSSL e o Mbed TLS se recusam a aceitá-lo (esses caracteres não estão no alfabeto deles), enquanto o GLib vai pular em silêncio o - e o _ e devolver uma string mais curta do que deveria - sem nenhum erro. Sempre passe pelo helper. A segunda é o próprio aviso do RFC, que vale a pena levar a sério: o base64url "não deve ser considerado o mesmo que a codificação base64". Se um payload acontece de não conter caracteres - ou _, os dois dialetos são idênticos byte a byte para esses dados, e uma confusão é invisível - o que é exatamente o motivo pelo qual a confusão sobrevive até bater num payload que contenha um.

Arquivos: restaurando o original

O trabalho em formato de arquivo mais comum é o inverso do que alguma rotina de exportação fez: um arquivo de texto .b64 chega, e você precisa do arquivo original de volta. Leia o texto inteiro, decodifique, e depois deixe os bytes se anunciarem antes de você confiar em qualquer rótulo. O C não tem finfo, então o teste prático é um farfalhar de magic numbers sobre os primeiros bytes:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("upload.b64", "rb");
  if (f == NULL) {
    return 1;
  }
  fseek(f, 0, SEEK_END);
  long size = ftell(f);
  fseek(f, 0, SEEK_SET);
  char *text = malloc((size_t)size + 1);
  size_t got = fread(text, 1, (size_t)size, f);
  fclose(f);
  text[got] = '\0';
  unsigned char *out = malloc((got * 3) / 4 + 3);
  int out_len = 0;
  if (decode_b64((const unsigned char *)text, (int)got,
      out, &out_len) != 0) {
    printf("not valid base64\n");
    free(text);
    free(out);
    return 1;
  }
  free(text);
  const char *kind = "unknown binary";
  if (out_len >= 4 && memcmp(out, "\x89PNG", 4) == 0) kind = "png";
  else if (out_len >= 5 && memcmp(out, "%PDF-", 5) == 0) kind = "pdf";
  else if (out_len >= 4 && memcmp(out, "PK\x03\x04", 4) == 0) kind = "zip";
  else if (out_len >= 3 && memcmp(out, "\xff\xd8\xff", 3) == 0) kind = "jpeg";
  printf("looks like a %s, %d real bytes\n", kind, out_len);
  free(out);
  return 0;
}

Notas nas bordas: abra o arquivo em modo binário (rb/wb) mesmo na metade de texto, porque o modo texto vai traduzir quebras de linha em algumas plataformas e corromper a sua contagem de caracteres; e nunca use printf("%s") no buffer decodificado para "ver o que é". O farfalhar de magic numbers é o jeito honesto de fazer essa pergunta, e se você depois servir o arquivo restaurado para um navegador, o Content-Type deve vir do mesmo farfalhar, não do nome do arquivo.

Data URIs: a imagem dentro da URL

Uma chegada favorita do mundo web: alguém cola uma imagem num formulário, e o front end entrega ao seu servidor um data URI completo como data:image/png;base64,iVBORw0KGgo.... O RFC 2397 define a forma: data:, um media type opcional, uma flag ;base64 opcional, uma vírgula, e aí o payload. Quando a flag está presente, o payload é Base64; quando está ausente, o payload é texto puro percent-encoded - mais raro, mas legal. Se o media type for omitido, o padrão é text/plain;charset=US-ASCII. Fazer o parse disso em C é uma questão de encontrar a vírgula e olhar o que fica logo antes dela:

int split_data_uri(const char *uri, char *mime, size_t mime_cap,
    int *is_b64, const char **payload) {
  if (strncmp(uri, "data:", 5) != 0) {
    return -1;
  }
  const char *comma = strchr(uri, ',');
  if (comma == NULL) {
    return -1;
  }
  *is_b64 = 0;
  const char *meta = uri + 5;
  size_t meta_len = (size_t)(comma - meta);
  if (meta_len >= 7 && strncmp(comma - 7, ";base64", 7) == 0) {
    *is_b64 = 1;
    meta_len -= 7;
  }
  if (meta_len == 0) {
    snprintf(mime, mime_cap, "text/plain;charset=US-ASCII");
  } else {
    snprintf(mime, mime_cap, "%.*s", (int)meta_len, meta);
  }
  *payload = comma + 1;
  return 0;
}

E o chamador lê como uma frase:

char mime[256];
int is_b64 = 0;
const char *payload = NULL;
const char *uri = "data:image/png;base64,iVBORw0KGgo...";
if (split_data_uri(uri, mime, sizeof(mime), &is_b64, &payload) == 0) {
  printf("mime=%s base64=%d\n", mime, is_b64);
  /* agora decodifique o payload com a biblioteca da sua escolha */
}

Três armadilhas vivem neste formato. A flag ;base64 ausente é a primeira: um data URI legal sem ela carrega um payload percent-encoded, e passar isso por um decodificador Base64 produz lixo - verifique a flag, e aí escolha o seu decodificador. O media type alegado é a segunda: é uma dica do remetente, não um fato; o farfalhar de magic numbers da seção de arquivos é o seu fato. A terceira é o tamanho: o próprio conselho do RFC é que data URIs são para valores curtos, então uma imagem de vários megabytes viajando dentro de uma URL é um cheiro na sua arquitetura, não um padrão para comemorar.

JWTs: lendo as partes que não são secretas

O payload Base64 mais famoso da web é o JSON Web Token, e o menos assustador uma vez que você conhece a forma dele. Conforme o RFC 7519, um JWT compacto é três partes base64url unidas por pontos: um header, um payload e uma assinatura - cada uma codificada sem padding, sem quebras de linha. As duas primeiras partes são JSON puro, por isso todo mundo consegue lê-las, e por isso todo mundo deveria ler até aqui antes de tocar num token.

Ler as duas primeiras partes são algumas linhas com o helper de base64url de cima, e é o jeito mais rápido de desmistificar um token:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int base64url_decode(const char *url_safe, unsigned char *out,
    size_t out_cap, size_t *out_len);
int main(void) {
  const char *token =
    "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
    "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
    "TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
  const char *dot1 = strchr(token, '.');
  if (dot1 == NULL) {
    return 1;
  }
  const char *part2 = dot1 + 1;
  const char *dot2 = strchr(part2, '.');
  if (dot2 == NULL) {
    return 1;
  }
  const char *part3 = dot2 + 1;
  char seg[512];
  char buf[1024];
  size_t n = 0;
  size_t hlen = (size_t)(dot1 - token);
  memcpy(seg, token, hlen);
  seg[hlen] = '\0';
  if (base64url_decode(seg, (unsigned char *)buf,
      sizeof(buf), &n) == 0) {
    printf("header:  %.*s\n", (int)n, buf);
  }
  size_t plen = (size_t)(dot2 - part2);
  memcpy(seg, part2, plen);
  seg[plen] = '\0';
  if (base64url_decode(seg, (unsigned char *)buf,
      sizeof(buf), &n) == 0) {
    printf("payload: %.*s\n", (int)n, buf);
  }
  printf("signature: %s (encoded, verify before trusting!)\n", part3);
  return 0;
}

Impresso, o header é {"alg":"HS256","typ":"JWT"} e o payload é {"sub":"1234567890","name":"John Doe"}. Agora a parte que importa: a terceira parte é uma assinatura, e as duas partes que você acabou de decodificar não são secretas nem autenticadas. Qualquer pessoa com um packet capture consegue lê-las, e qualquer pessoa com um editor de texto consegue reescrevê-las. Confiar num payload de JWT em C antes de verificar a assinatura é o bug clássico de autenticação, e o Base64 torna fácil não perceber - o token parece um blob inquebrantável enquanto é um cartão-postal. Para verificar um token HS256 você recalcula o HMAC-SHA256 sobre header.part com o seu segredo usando HMAC() de <openssl/hmac.h> e compara em tempo constante com CRYPTO_memcmp(); se os digests não coincidirem, o token é rejeitado, sem importar o que ele alega. Não existe uma biblioteca de JWT padrão de facto em C, então para produção você vai construir esse pequeno passo de verificação você mesmo ou adotar uma das bibliotecas da comunidade - mas o lado Base64 do trabalho é a dança de separar-e-decodificar acima, e você deveria entender todo ele.

Basic Auth: o header que nunca aprendeu privacidade

O header de autenticação mais antigo da web ainda viaja em cima de Base64: Authorization: Basic seguido da codificação em alfabeto padrão de username:password (RFC 7617, que o RFC 911 referencia para o esquema Basic). O RFC é explícito em dizer que isso é codificação, não proteção - qualquer pessoa com um packet capture decodifica as duas metades num comando - então o trabalho do lado da decodificação em C é fazer o parse do header, decodificar com estriteza, separar no primeiro dois-pontos (senhas podem conter dois-pontos legalmente), e comparar com uma função segura contra timing:

#include <string.h>
#include <openssl/evp.h>
#include <openssl/crypto.h>
static size_t real_length(const char *b64);
int basic_auth_ok(const char *header, const char *expected_user,
                  const char *expected_pass) {
  if (strncmp(header, "Basic ", 6) != 0) {
    return 0;
  }
  const char *b64 = header + 6;
  unsigned char out[256];
  int n = EVP_DecodeBlock(out, (const unsigned char *)b64,
                          (int)strlen(b64));
  if (n < 0) {
    return 0;
  }
  size_t real = real_length(b64);
  size_t u_len = strlen(expected_user);
  size_t p_len = strlen(expected_pass);
  if (real != u_len + 1 + p_len) {
    return 0;
  }
  if (memcmp(out, expected_user, u_len) != 0) {
    return 0;
  }
  if (out[u_len] != ':') {
    return 0;
  }
  return CRYPTO_memcmp(out + u_len + 1, expected_pass, p_len) == 0;
}

A verificação de comprimento está fazendo trabalho de verdade: ela impede que um payload que decodifica para "alice:secret" com lixo no final, ou "alice:secre" truncado, case. E o CRYPTO_memcmp (ou memcmp só se você entende as implicações de timing) é o que impede um atacante de medir o tempo e passar pela sua lista de usuários. Sirva este header por HTTPS ou nem sirva - numa conexão pura, a camada Base64 é enfeite de vitrine.

Email e PEM: o lar original

O Base64 nasceu para um problema bem específico: o transporte de email só carregava ASCII de 7 bits, e as pessoas queriam enviar binários por ele. O MIME (RFC 2045) transformou o Base64 numa das codificações de transferência padrão e acrescentou duas regras de casa: linhas codificadas não devem exceder 76 caracteres, e o software de decodificação deve ignorar caracteres fora do alfabeto - quebras de linha incluídas. Essa segunda regra é a razão pela qual os decodificadores streaming acima mastigam um anexo quebrado com zero pré-processamento, e é por isso que o hábito dos 76 caracteres ainda está assentado em toda biblioteca de email do planeta. O ancestral foi o PEM (Privacy Enhanced Mail, RFC 1421), que usava linhas de 64 caracteres no lugar - a divisão 64/76 que você vê em ferramentas é essa história, os dois limites no fim das contas impostos pelo SMTP.

A armadura PEM - o formato pelo qual chaves e certificados viajam - é só um Base64 rotulado: uma linha -----BEGIN ... -----, o corpo em linhas de 64 caracteres, e uma linha END correspondente. Tirar a armadura em C é uma varredura linha a linha, e aí o decodificador faz o resto:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("server.key", "r");
  if (f == NULL) {
    return 1;
  }
  char line[256];
  char b64[8192];
  size_t pos = 0;
  int in_body = 0;
  while (fgets(line, sizeof(line), f) != NULL) {
    if (strncmp(line, "-----BEGIN", 10) == 0) {
      in_body = 1;
      continue;
    }
    if (strncmp(line, "-----END", 8) == 0) {
      in_body = 0;
      break;
    }
    if (in_body) {
      size_t l = strlen(line);
      while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r')) {
        l--;
      }
      memcpy(b64 + pos, line, l);
      pos += l;
    }
  }
  fclose(f);
  unsigned char der[8192];
  int out_len = 0;
  if (decode_b64((const unsigned char *)b64, (int)pos,
      der, &out_len) != 0) {
    printf("armor contained no valid base64\n");
    return 1;
  }
  printf("DER payload decoded\n");
  return 0;
}

Os bytes decodificados são DER, uma serialização binária compacta, e é isso que as funções de certificado e chave do OpenSSL consomem no final. Duas notas: colete o corpo sem as quebras de linha (como o laço faz) para o seu comprimento ser um múltiplo de quatro, e se um arquivo carrega vários blocos, case o rótulo END com o rótulo BEGIN que você abriu - uma flag simples funciona quando você só quer o primeiro bloco, como aqui.

Segredos, configs e colunas de banco de dados

O Base64 é um contêiner de texto, por isso ele continua aparecendo em lugares que você não esperaria. Em arquivos de configuração e variáveis de ambiente, é o truque para contrabandear valores que de outro modo quebrariam o formato: um DSN de banco com ponto-e-vírgulas, uma senha com aspas, um valor com quebra de linha. Em bancos de dados, um blob binário pode morar numa coluna de texto como Base64 e sobreviver a toda ferramenta que assume texto - a um custo, porém, de mais ou menos um terço a mais de tamanho, então dimensione suas colunas de acordo (ou pergunte por que o valor não está simplesmente numa coluna BLOB).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
static size_t real_length(const char *b64) {
  size_t len = strlen(b64);
  while (len > 0 && b64[len - 1] == '=') len--;
  return len * 3 / 4;
}
int main(void) {
  const char *b64 = getenv("API_KEY_B64");
  if (b64 == NULL) {
    printf("API_KEY_B64 is not set\n");
    return 1;
  }
  size_t cap = strlen(b64);
  unsigned char *out = malloc(cap);
  int n = EVP_DecodeBlock(out, (const unsigned char *)b64, (int)cap);
  if (n < 0) {
    printf("API_KEY_B64 is not valid base64\n");
    free(out);
    return 1;
  }
  size_t real = real_length(b64);
  printf("key is %zu bytes\n", real);
  free(out);
  return 0;
}

O cuidado se aplica duas vezes. Primeiro, isso é segurança de formato, não sigilo: no momento em que um desenvolvedor consegue ler o arquivo de configuração, ele decodifica o valor numa chamada, e a seção de segurança do RFC registra incidentes reais em que pessoas reportaram uma troca de protocolo ao suporte e "revelaram a senha por acidente" porque o Base64 é visualmente disfarçado, não computacionalmente protetor. Nunca guarde um segredo como Base64 e chame de cifrado. Segundo, valide na inicialização: um valor de ambiente meio colado é um -1 da chamada estrita, e uma verificação de uma linha transforma uma falha enigmática três horas depois numa mensagem acionável já na partida.

Decodificando da shell

Nem toda decodificação acontece dentro do seu programa. Scripts de CLI, jobs de cron e one-liners decodificam Base64 o tempo todo, e desenvolvedores de C deveriam conhecer as duas ferramentas que já existem em toda máquina Linux. A ferramenta do coreutils é a geral: base64 -d decodifica, -i faz ela ignorar caracteres de lixo em vez de falhar, e -w define a coluna de quebra de linha (o que só afeta a codificação, não a decodificação):

base64 -d < blob.b64 > blob.bin
base64 -d -i < messy.b64 > blob.bin

O OpenSSL traz o seu próprio, acessível como openssl base64 (um alias mais amigável de openssl enc -base64):

openssl base64 -d < blob.b64 > blob.bin
openssl base64 -d -A < blob.b64 > blob.bin

A flag -A significa "uma linha só": codifique sem a quebra de linha de 64 caracteres, e espere que a entrada também seja uma linha única. E aqui está uma armadilha de CLI que vai te custar uma noite se você não ler: a decodificação base64 do OpenSSL é orientada a linhas, e um payload que chega sem nenhuma quebra de linha decodifica para nada, em silêncio:

printf 'TQ=='  | openssl base64 -d | wc -c   # 0
printf 'TQ==\n' | openssl base64 -d | wc -c  # 1

O decodificador do coreutils não tem essa expectativa, o que é um dos motivos por ser o padrão mais seguro para trabalho de cola. Mais uma nota de dialeto: sistemas derivados de BSD (o macOS mais antigo em particular) historicamente escreviam a flag de decodificação como -D; as versões modernas seguem a convenção GNU de -d, então cheque a man page na máquina em que você realmente está.

Payloads grandes, memória pequena

Decodificar é a direção que te ajuda: a saída tem três quartos do tamanho da entrada, então pressão de memória vinda do Base64 é rara. Ainda assim, quando um arquivo .b64 de várias centenas de megabytes pousa no disco, o caminho streaming de antes é a sua ferramenta, e ele é mais simples do que parece. Leia o arquivo codificado em pedaços, alimente cada pedaço no EVP_DecodeUpdate, e escreva os bytes decodificados à medida que chegam. O contexto segura de um a três caracteres de qualquer grupo inacabado entre as chamadas, então as fronteiras dos pedaços podem cair em qualquer lugar - você não precisa alinhá-las:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_DecodeInit(ctx);
  FILE *in = fopen("huge.b64", "rb");
  FILE *outf = fopen("huge.bin", "wb");
  char inbuf[65536];
  unsigned char outbuf[49152 + 4];
  size_t got;
  int ok = 1;
  while (ok && (got = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
    int outl = 0;
    int r = EVP_DecodeUpdate(ctx, outbuf, &outl,
        (const unsigned char *)inbuf, (int)got);
    if (r < 0) {
      ok = 0;
    } else if (outl > 0) {
      fwrite(outbuf, 1, (size_t)outl, outf);
    }
  }
  int tail = 0;
  if (ok && EVP_DecodeFinal(ctx, outbuf, &tail) == 1 && tail > 0) {
    fwrite(outbuf, 1, (size_t)tail, outf);
  }
  EVP_ENCODE_CTX_free(ctx);
  fclose(in);
  fclose(outf);
  return ok ? 0 : 1;
}

A memória de pico são dois buffers na ordem de algumas dezenas de quilobytes, independente do tamanho do arquivo, e um arquivo corrompido falha rápido - o EVP_DecodeUpdate devolve -1 no pedaço onde o dano está, então você pode reportar um offset em vez de dar de ombros. Um aviso de biblioteca para este caminho: o decodificador do APR-Util trabalha em strings terminadas em NUL com contabilidade de tamanho int (o valor de retorno dele é um int e a entrada é limitada a pouco menos de 3 GB por uma constante interna), então ele fica fora da corrida para arquivos de vários gigabytes. Se você precisa de relatório de progresso, conte os bytes que você escreveu - essa é a sua posição na saída, e a posição de entrada é mais ou menos quatro terços dela.

As armadilhas, todas específicas de C

Reunidas num só lugar, as armadilhas que são específicas de fazer isso em C:

  • O one-shot com zero-padding. O EVP_DecodeBlock devolve o comprimento do quanta, não o comprimento dos dados. TQ== reporta três bytes mas carrega um. Recalcule sempre o comprimento real a partir dos pads do final, ou use o par streaming.
  • Bytes decodificados não são uma string. O resultado pode conter bytes NUL e pode não ser UTF-8. Sem strlen, sem printf("%s"), sem passar para funções que assumem texto. Leve (ponteiro, comprimento) para todo lugar.
  • O tamanho do buffer é trabalho seu. O C não vai crescer o seu buffer de saída, e os decodificadores também não - o update do OpenSSL escreve o que decodifica no espaço que você deu. Dimensione em in_len * 3 / 4 + 3 (mais o sobrecusto de quebra de linha se a entrada tiver quebra de linha e você decodifica com um helper que não corta) e mantenha uma verificação de teto em todo wrapper.
  • Consultas com char assinado. Se um dia você escrever um decodificador você mesmo, o bug clássico é usar o byte de entrada como índice numa tabela de 256 entradas com um char comum numa plataforma onde char é assinado: o byte 0xFF vira -1 e você indexa para trás pela memória. Indexe com valores unsigned char ou unsigned, sempre.
  • As silenciosas são as perigosas. O APR-Util para no primeiro caractere inválido e não diz nada; o GLib pula lixo e não diz nada. OpenSSL e Mbed TLS falham com estrondo. Se a sua entrada é não confiável, o silêncio da biblioteca é um bug no seu programa, não na biblioteca.
  • A linha de comando come quebras de linha. O openssl base64 -d decodifica zero bytes se a entrada não tiver quebra de linha. Pipelines de shell que cortam quebras de linha finais (tr -d '\n', xargs, salvamentos de editor sem nova linha final) vão produzir saída vazia sem erro nenhum.
  • int versus size_t. A API one-shot do OpenSSL aceita um comprimento int, o APR-Util usa int o tempo todo, e as APIs do Mbed TLS e do GLib usam size_t. Aritmética de comprimentos misturados entre eles é onde avisos de assinado/não-assinado escondem bugs reais - e onde mora o teto de 2 GB do APR.
  • Espaços em branco não são uniformes. O OpenSSL pula todos os espaços em branco em qualquer lugar; o Mbed TLS permite CRLF/LF entre grupos e espaços logo antes de uma quebra, mas não depois de uma ou no meio da linha; as ferramentas de CLI variam. Um payload que é válido para um decodificador pode ser inválido para outro, e "funcionou na minha máquina" geralmente significa "o meu decodificador era mais preguiçoso".

Bons hábitos, reunidos

Valide antes de confiar: uma verificação de forma (caracteres do alfabeto, no máximo dois pads no final) pega lixo óbvio antes de qualquer decodificação, mas só uma decodificação de verdade entende a semântica do Base64, então o decodificador estrito tem a palavra final. Use o par streaming do OpenSSL quando você precisa de comprimentos honestos ou entrada em pedaços, e o one-shot quando o payload é pequeno e você corrige o comprimento dele na hora. Mantenha os pares (ponteiro, comprimento) juntos e nunca deixe um buffer decodificado encontrar uma função de string. Compare material de autenticação com CRYPTO_memcmp. Farfalte os magic bytes antes de acreditar num nome de arquivo ou num tipo MIME alegado. E trate o Base64 pelo que ele é - um formato de embalagem, uma caixinha para bytes - e não como uma tranca: nada nesses 64 caracteres torna os seus dados privados.

Uma breve história do Base64 em C

A história começa com email. Em 1990 e 1991 um grupo de criptógrafos esboçou o Privacy Enhanced Mail, um sistema de email assinado e cifrado, e eles precisavam de um jeito de carregar binário através de uma rede de 7 bits. A resposta deles, padronizada como RFC 1421 em 1993, codificava dados com seis bits por caractere - "base 64" - em linhas de 64 caracteres, e a implementação era, claro, em C. Mais ou menos ao mesmo tempo a web chegou com o seu próprio MIME, o RFC 1521 (1993) e depois o RFC 2045 (1996), que manteve o mesmo alfabeto, relaxou o comprimento da linha para 76, e transformou o Base64 no formato de anexo da internet recém-nascida.

A biblioteca padrão do C perdeu o barco inteiro. O padrão C89 foi publicado em 1990, três anos antes do MIME, e o comitê da linguagem nunca acrescentou uma função Base64 desde então - nem no C99, nem no C11, nem no C23 (a revisão de 2024). Então o ecossistema cresceu em volta das bibliotecas: o OpenSSL carrega as rotinas EVP de codificação/decodificação no libcrypto desde que alguém linka OpenSSL para TLS, o Mbed TLS (renomeado de PolarSSL em 2015) manteve um par pequeno e estrito para sistemas embedded, o APR-Util saiu com o Apache quando o servidor precisou decodificar os próprios headers de autenticação, e o GLib acrescentou o trio dele para o desktop. Os padrões perseguiram as implementações: o RFC 3548 em 2003 arrumou as definições antigas, e o RFC 4648 em 2006 (Base-N Encodings) formalizou os alfabetos, a variante URL-safe, e as regras de segurança nas quais este artigo se apoia. Apropriadamente, a seção 11 daquele RFC aponta para uma implementação de referência em ISO C99 - o decodificador de exemplo do próprio padrão é escrito em C, o que te diz tudo sobre onde este formato mora.

Fatos curiosos, edição C

Alguns fatos com sabor de C que são simplesmente divertidos de saber:

  • O nome é matemática, não marketing: cada caractere de saída carrega exatamente seis bits, e 2 elevado a 6 é 64. "Base64" é a base, lida em voz alta.
  • O alfabeto tem 65 caracteres, não 64: os 64 símbolos mais o =, que o RFC 4648 chama de "o 65º caractere extra" usado para uma função especial de processamento. O pad é um trabalhador, não uma letra.
  • O OpenSSL quebra a saída codificada em 64 caracteres (o hábito do PEM) enquanto o coreutils quebra em 76 (o hábito do MIME). A diferença de 12 caracteres são duas décadas de história de email que você pode ver na saída de dois comandos na mesma máquina.
  • O autor do comando base64 do GNU coreutils é Simon Josefsson - a mesma pessoa que escreveu o RFC 4648. O padrão e uma de suas implementações mais usadas compartilham um autor, e é assim que os dois acabaram concordando sobre cada caso limite.
  • O Mbed TLS faz suas consultas de tabela através de helpers de tempo constante (mbedtls_ct_base64_*), então a velocidade de decodificação não vaza quais caracteres ele viu. Um detalhe que você nunca vai notar e que te deixa feliz por existir.
  • TQ== é o payload não trivial mais pequeno: um byte de verdade, dois pads. É o vetor de teste perfeito - o decodificador one-shot do OpenSSL devolve três bytes para ele, o decodificador streaming dele devolve um, o Mbed TLS devolve um, e o GLib devolve um. Quatro bibliotecas, duas respostas, e a diferença é o zero-padding.
  • As funções base64 do APR são as únicas deste artigo que se importam com EBCDIC, porque o httpd ainda roda em máquinas onde as letras não são ASCII. A biblioteca padrão do C nunca conheceu um mainframe; o APR conheceu.
  • O payload vazio é a identidade universal: toda biblioteca codifica e decodifica entrada de comprimento zero para saída de comprimento zero, sem erro. Se o seu decodificador engasgar com uma string vazia, você tem um bug, não um formato.

Virando para o lado do codificador

Essa era a parte do decodificador, e é onde mora a maior parte da dor, porque decodificar é onde você encontra os dados dos outros: as escolhas de padding deles, as quebras de linha deles, os bytes corrompidos deles, os tokens deles. A direção oposta - transformar bytes numa string Base64 - é um animal mais calmo com o seu próprio elenco de armadilhas: matemática exata de buffers, a pergunta da quebra de linhas, e a fatura de tamanho que cai em todo remetente. A codificação Base64 em C é coberta em profundidade no artigo relacionado, linkado a partir desta página, e ele faz par com este da mesma forma que um decodificador faz par com um codificador: leia os dois e você nunca mais será surpreendido por nenhuma das duas direções.

Última atualização: 2026-09-08

Artigo relacionado: Codificação Base64 em C: um guia completo