Enviar um arquivo
  1. base64hub
  2. Base64 no código
  3. C
Código

Base64 em C

A biblioteca padrão do C não tem funções de Base64, então a maioria dos projetos carrega a própria. Aqui está uma implementação curta, testada com fuzzing contra o módulo base64 do Python, mais os detalhes que costumam dar errado.

Codificador

Cada 3 bytes de entrada viram 4 caracteres, cada um com 6 bits. Um grupo final de 1 ou 2 bytes é completado com =. A página como o Base64 funciona mostra a disposição dos bits passo a passo.

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

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

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

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

A condição do laço i + 2 < n em vez de i < n - 2 faz diferença: com size_t, n - 2 dá a volta e vira um número enorme quando n é 0 ou 1. Os casts para uint32_t evitam que in[i] << 16 seja calculado num int com sinal.

Decodificador rígido

O decodificador rejeita tudo o que não é Base64 canônico com preenchimento: comprimento errado, caracteres fora do alfabeto, preenchimento no meio e bits de preenchimento diferentes de zero.

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

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

O b64_val recebe um unsigned char para que bytes acima de 127 não virem índices negativos em plataformas onde char tem sinal. Uma tabela de consulta de 256 entradas é mais rápida; a cadeia de if é mais fácil de conferir a olho.

Uso e resultados

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

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

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

O C não tem noção de codificação de texto: os bytes do literal de string são os que estiverem no arquivo-fonte. Com um arquivo-fonte em UTF-8 e o charset de execução padrão do GCC, você obtém UTF-8.

O que o b64_decode retorna para algumas entradas típicas:

EntradaRetornoMotivo
""0vazio é válido
aGk=2hi
aGk-1comprimento não é múltiplo de 4
aGk=!!-1comprimento e caracteres
aGl=-1bits de preenchimento diferentes de zero
PDw_Pz8-Pg==-1caracteres URL-safe
a===-1preenchimento demais

Compilamos com gcc -std=c99 -Wall -Wextra -fsanitize=address,undefined, fizemos ida e volta com 2.000 buffers aleatórios e comparamos a decisão de aceitar ou rejeitar em 20.000 strings aleatórias com o b64decode(validate=True) do Python mais uma verificação por recodificação. Nenhuma diferença e nenhum alerta dos sanitizers. Para testar as suas próprias strings, o validador de Base64 aplica as mesmas regras.

Codificar um arquivo ou a stdin em blocos

Leia blocos com tamanho múltiplo de 3, e a saída concatenada será igual à codificação do arquivo inteiro de uma vez:

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

Num arquivo aleatório de 1.000.003 bytes, ./b64 < big.bin gerou a mesma saída que base64 -w 0 big.bin. O fread continua lendo até encher o buffer ou a entrada acabar, então leituras curtas de um pipe não inserem preenchimento. No Windows, chame _setmode(_fileno(stdin), _O_BINARY) antes, senão a tradução de CRLF corrompe a entrada binária.

Saída URL-safe

Converta depois de codificar: troque dois caracteres e remova o preenchimento.

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

Para decodificar, mapeie - para 62 e _ para 63 no b64_val e aceite um grupo final de 2 ou 3 caracteres sem =. Veja Base64 vs Base64URL e preenchimento explicado.

Bugs comuns em Base64 escrito à mão

  • Buffers com erro de um. Esquecer o terminador NUL ou dimensionar a saída como n * 4 / 3, que fica pequeno demais sempre que n não é múltiplo de 3. A calculadora de tamanho e o guia de aumento de tamanho dão os números exatos.
  • Consultas com char com sinal. table[c] com um c negativo lê fora do array.
  • strlen em dados binários. A saída decodificada pode conter 0x00; use o tamanho retornado, não o strlen.
  • Trígrafos. Nos modos estritos de C99/C11, uma string de teste como "<<???>>" contém o trígrafo ??>, que o compilador transforma em }. O GCC avisa com -Wtrigraphs. Escape como "<<?\?\?>>" ou use um array de bytes. O C23 removeu os trígrafos.

A versão deste código em C++, com std::string e std::vector, está em Base64 em C++.

Perguntas frequentes

De que tamanho precisa ser o buffer de saída?

Para codificar, 4 * ((n + 2) / 3) caracteres mais um byte para o NUL final. Para decodificar, 3 * (len / 4) bytes sempre bastam; o tamanho real é até dois bytes menor por causa do preenchimento.

Existe alguma função de Base64 no OpenSSL ou na glibc que eu possa usar?

O OpenSSL tem EVP_EncodeBlock e EVP_DecodeBlock, e a camada BIO dele consegue processar Base64 em fluxo. A glibc não tem API pública de Base64. Se o seu projeto já usa uma dessas bibliotecas, faz sentido aproveitá-la; caso contrário, o código abaixo dá menos trabalho que uma dependência nova.

Por que o meu decodificador aceita strings como aGl=?

aGl= e aGk= decodificam para hi; a primeira tem bits de preenchimento diferentes de zero. Decodificadores tolerantes ignoram esses bits. O decodificador desta página os rejeita verificando (c & 3) e (b & 15) no último grupo.

Continue lendo