Devi lavorare con il formato Base64? Allora questo sito è perfetto per te! Usa il nostro praticissimo strumento online per codificare o decodificare i tuoi dati.

Codifica Base64 in C: una guida completa

Hai dei byte. Magari sono una JPEG letta da disco, magari un token, magari una password che un client sta per trasmettere, magari i byte grezzi di un file che qualche pipeline si aspetta dentro un campo JSON. E più avanti sulla strada c'è un canale che parla solo testo: una stringa JSON, un URL, il corpo di un'email, un file di configurazione, una colonna di database che finge di essere testo. Ed è qui che entra in gioco la codifica Base64: riscrive ogni tre byte di dati grezzi come quattro caratteri di un alfabeto di 64 lettere, così il risultato è ASCII puro che sopravvive a qualsiasi pipeline di testo sulla faccia della terra. La home page di questo sito spiega il formato per intero; questo articolo è su come fare il lavoro bene in C, dove - come al solito - il linguaggio non lo fa per te.

L'unico numero da tenere in testa: la codifica ingrandisce i tuoi dati. Tre byte diventano quattro caratteri, quindi ogni payload lascia il tuo programma circa un terzo più grande, e ancora un po' di più ogni volta che vengono aggiunti a capo. Questa è la tassa, e non c'è modo di evitarla - ma in C la tassa ha una voce di dettaglio, perché il buffer di output lo allochi tu e il buffer deve essere esattamente grande a sufficienza. Fai bene i conti una volta e ogni encoder di questo articolo diventa prevedibile: nessun overflow, nessun underflow, nessun chiedersi dove atterrerà il prossimo byte. Poi c'è la cassetta degli attrezzi da cui scegliere - OpenSSL, Mbed TLS, APR-Util, GLib - e la scelta conta, perché ognuno avvolge in modo diverso, termina in modo diverso e dà errori in modo diverso.

Quattro encoder, quattro personalità

Tutte e quattro le librerie codificano l'alfabeto standard in modo corretto e identico - stessi byte dentro, stessi caratteri fuori, sempre. Le differenze stanno nell'imballaggio, ed è nell'imballaggio che si nascondono i bug di interoperabilità. Ecco il panorama:

Libreria Intestazione Stile di output Modo di fallimento
OpenSSL (libcrypto) <openssl/evp.h> Nessun a capo; scrive un terminatore NUL In pratica nessuno (solo allocazione)
Mbed TLS <mbedtls/base64.h> Nessun a capo; terminato con NUL Codice buffer-troppo-piccolo con la dimensione richiesta
APR-Util <apr-1.0/apr_base64.h> Nessun a capo; aggiunge un NUL Nessuno - fidati della dimensione del buffer
GLib <glib.h> Nessun a capo; terminato con NUL, allocato in heap Restituisce NULL (solo allocazione)

Nota quello che manca dalla tabella: nessuno di loro avvolge le righe di default. È deliberato - RFC 4648 dice che le implementazioni non devono aggiungere a capo a meno che la specifica circostante non lo chieda esplicitamente - ed è un sollievo, perché un a capo perso dentro una stringa JSON o un URL è un errore, non una caratteristica. L'avvolgimento esiste per email e PEM, e quando ti serve o lo prendi dal percorso streaming di OpenSSL o lo fai da solo in cinque righe (la sezione email mostra entrambi). Per scegliere una libreria: usa OpenSSL se la linki già, Mbed TLS per build embedded dove ogni kilobyte si discute, APR-Util dentro l'ecosistema Apache, e GLib quando il resto del tuo programma è già GLib. Installazione: libssl-dev (Debian/Ubuntu) o openssl-devel (Fedora/RHEL) o brew install openssl (macOS); libmbedtls-dev per Mbed TLS; libaprutil1-dev più libapr1-dev per APR-Util; glib2.0-dev per GLib.

Fai i conti prima di allocare

Prima di qualsiasi codice, l'aritmetica, perché C non ti salva da un buffer troppo piccolo. Ogni tre byte di input producono esattamente quattro caratteri di output. Se la lunghezza dell'input non è un multiplo di tre, l'ultimo gruppo produce comunque quattro caratteri e gli slot non usati sono marcati con i pad =: un byte di input diventa quattro caratteri con due pad, due byte di input diventano quattro caratteri con un pad. Quindi la lunghezza codificata esatta per n byte è:

size_t encoded_chars(size_t n) {
  return ((n + 2) / 3) * 4;
}

Per 1000 byte sono 1336 caratteri; per 1 byte sono 4; per 0 sono 0. Due aggiustamenti ne derivano. Prima, OpenSSL e Mbed TLS aggiungono entrambi un terminatore NUL dopo i dati (e Mbed TLS riserva lo spazio per lui quando chiedi la dimensione), quindi il tuo buffer vuole un byte in più: encoded_chars(n) + 1. Secondo, se vuoi un output con righe avvolte, aggiungi un a capo per riga: l'encoder streaming di OpenSSL emette una riga da 64 caratteri per ogni 48 byte di input, quindi la lunghezza avvolta è encoded_chars(n) + (n + 47) / 48. Verifica con 1000 byte: 1336 caratteri più 21 a capo fanno 1357, ed è esattamente ciò che l'encoder produce. Scrivi la formula una volta come funzione e usala ovunque; è la differenza tra "ci sta" e una corruzione dell'heap alle 3 di notte.

size_t b64_buffer_size(size_t in_len) {
  return ((in_len + 2) / 3) * 4 + 1; /* caratteri + NUL */
}
size_t b64_buffer_size_wrapped(size_t in_len) {
  return ((in_len + 2) / 3) * 4 + (in_len + 47) / 48 + 1;
}

OpenSSL: un blocco o un rubinetto aperto

La funzione a colpo singolo di OpenSSL è il cavallo da lavoro, ed è la più amichevole del gruppo:

#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;
}

Scrive i caratteri codificati in out, aggiunge un NUL dopo di essi, e restituisce la lunghezza senza il NUL - quindi la stampa con %s è sicura e la lunghezza è disponibile se ti serve. Il buffer di output deve contenere encoded_chars(n) + 1 byte. Non c'è un percorso di errore da gestire: la codifica non può fallire, perché qualsiasi byte è input legale, e la funzione non ha alcun concetto di validazione dell'input su cui inciampare. L'unico modo in cui può andare storto è che tu gli dia un buffer troppo piccolo, e la sezione sui conti è l'antidoto.

La coppia streaming è per quando i dati sono grandi o arrivano a pezzi. EVP_EncodeUpdate elabora l'input a blocchi da 48 byte e scrive 64 caratteri più un a capo (65 byte) per ogni blocco completo, tenendo ogni resto nel contesto finché non arriva altro dato o la chiamata finale:

#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 caratteri + 21 a capo + margine */
  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;
}

L'output di quel programma sono 1357 byte con 21 a capo - la formula della sezione sui conti, resa reale. Due note pratiche. La nota sulla versione: dal 2016, con OpenSSL 1.1.0, il tipo contesto è opaco, quindi alloca con EVP_ENCODE_CTX_new() e libera con EVP_ENCODE_CTX_free(); il vecchio pattern in stack EVP_ENCODE_CTX ctx; che trovi in tutorial mirati a 1.0.2 e precedenti non compila contro i file di intestazione moderni, OpenSSL 3.x inclusa. E la nota di design: dato che da EVP_EncodeUpdate escono solo blocchi completi da 48 byte, la pipeline a blocchi più pulita gli dà multipli di 48 - allora ogni riga che la funzione scrive è una riga finita, e EVP_EncodeFinal da solo decide come viene avvolta la coda. Se il tuo input arriva in dimensioni arbitrarie (una lettura di rete), il contesto gestisce comunque l'allineamento per te; l'abitudine del multiplo di 48 è solo ciò che rende l'output prevedibile.

Mbed TLS: chiedi, poi codifica, ottieni una stringa

La codifica di Mbed TLS ha il contratto più pulito del gruppo, costruito attorno a una query sulla dimensione che puoi chiamare con una destinazione 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;
}

Leggi i dettagli con attenzione, perché sono una masterclass di API amichevole. La query sulla dimensione riporta needed come i caratteri codificati più uno per il NUL - per "Mane" sono 8 più 1, cioè 9 - e si segnala con il codice "buffer troppo piccolo" (MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL, che è -0x002A), perché una destinazione NULL è, per definizione, troppo piccola. La chiamata vera poi scrive i caratteri e il NUL, e *olen torna come 8 - la lunghezza senza il terminatore - quindi il buffer è già una stringa C stampabile. Se gli passi un buffer corto di un byte, ti torna lo stesso codice "troppo piccolo" con la dimensione richiesta in *olen, quindi il fallimento ti dice esattamente di quanto ti sei sbagliato. Un'ultima nota: la libreria fa le ricerche in tabella attraverso helper in tempo costante, un piccolo tocco di attenzione che non vedi nella maggior parte degli encoder.

APR-Util e GLib: gli altri due

L'encoder di APR-Util è una semplice coppia di funzioni con lunghezze 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;
}

Qui apr_base64_encode_len() e il valore di ritorno contano entrambi il NUL, quindi n è uno in più rispetto al conteggio dei caratteri - una differenza di contabilità rispetto a OpenSSL e Mbed TLS che ha prodotto bug off-by-one in più di una codebase. Vale lo stesso limite di lunghezza a 32 bit: per valori vicini o superiori a 2 GB, questo non è lo strumento giusto. C'è anche apr_base64_encode_binary(), che sulle macchine EBCDIC salta la conversione EBCDIC-ASCII dell'input - sui mainframe dove quella conversione altrimenti avverrebbe, e una differenza no-op dappertutto altrove. Quella coppia, la variante binaria e le funzioni di decodifica corrispondenti sono di fatto l'intera superficie base64 che apr-util espone: non esiste una variante allocata dalla pool e nessuna variante streaming, quindi il pattern malloc di sopra è l'unico pattern.

L'encoder di GLib è dello stile allocato in heap - ottieni una stringa terminata con NUL e un dovere:

#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;
}

Nessun avvolgimento, terminata con NUL, liberala con g_free - l'annotazione G_GNUC_MALLOC sul prototipo è ciò che lo comunica agli analyzer statici. Quando vuoi davvero gli a capo, la coppia incrementale è lo strumento: g_base64_encode_step() prende un intero di stato e un flag break_lines e ti dice quanti byte di output ha scritto, e g_base64_encode_close() chiude l'ultimo gruppo parziale. È la stessa forma a macchine a stati della coppia streaming di OpenSSL, solo con lo stile di parametri di GLib.

Base64 URL-Safe fatto a mano

Le quattro librerie di sopra parlano tutte l'alfabeto standard: A-Z, a-z, 0-9, più e barra. Il web, però, parla sempre più il secondo dialetto della sezione 5 di RFC 4648, chiamato base64url: la stessa codifica con + scambiato con - e / scambiato con _, e il padding finale = buttato quando la lunghezza è nota. JSON Web Token, parametri di stato OAuth e innumerevoli ID di API lo usano, perché + e / sono entrambi pericolosi negli URL, mentre - e _ sono caratteri non riservati che attraversano tutto. Dato che nessuna libreria C emette questo dialetto nativamente, lo fai da solo - e sono due piccole modifiche, perché hai già un encoder in alfabeto standard:

#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 buttato apposta */
}

L'uso è in due passi - codifica standard, poi traduzione:

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 */

Due avvertenze. Il ciclo si ferma al primo =, ed è questo a buttare il padding - non "correggerlo", buttare il padding è il punto (il ricevente che ne ha bisogno può rimetterlo dalla lunghezza). E dimensiona out per la lunghezza codificata completa, non meno: la traduzione è carattere per carattere fino ai pad, quindi la capacità che hai già allocato per la forma standard va esattamente bene. Una nota onesta per l'interoperabilità: se i tuoi dati capitano di non contenere byte che mappano su + o /, le forme standard e URL-safe sono identiche e nulla si lamenterà mai di uno scambio - il bug emerge solo quando i dati finalmente ne contengono uno. Tratta il dialetto come una proprietà del canale (URL, token), non dei dati.

Testo e set di caratteri: UTF-8 è solo byte

Una domanda che sorprende i neofiti del C: che succede al testo accentato, alle emoji, ai caratteri CJK? La risposta è il fatto più liberante di questo articolo - non deve succedere nulla. Il Base64 lavora sui byte, e C è un linguaggio di byte. Se il tuo testo è UTF-8 (e nel 2026 probabilmente lo è), la codifica UTF-8 di "café" sono cinque byte - 63 61 66 c3 a9 - e il Base64 codifica quei cinque byte esattamente come altri cinque byte qualsiasi, producendo Y2Fmw6k=. Nessun parametro di set di caratteri, nessun BOM, nessun passo di conversione, nessuna chiamata di libreria. Il codec non sa e non gli importa cosa significano i byte; quello è l'intero design.

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *utf8 = "caf\303\251"; /* café in 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;
}

Due trappole stanno ai bordi di questa sezione. La prima è wchar_t: se i tuoi dati sono arrivati come caratteri ampi, devi prima convertirli in una sequenza di byte (su Linux, UTF-8, tramite wcstombs() o il tuo meccanismo di locale) prima di codificare - il Base64 di un array wchar_t è la codifica di una rappresentazione interna, non del testo, e varierà tra piattaforme. La seconda è la codifica del sorgente: il letterale di stringa nel tuo file C è codificato nella codifica del file sorgente (UTF-8 in qualsiasi progetto moderno), quindi scrivere "café" direttamente funziona finché il file è davvero UTF-8 e il tuo compilatore viene informato (e lo è, di default, nelle toolchain moderne). Codifica i byte che intendi inviare, e lascia al ricevente il compito di decidere cosa significano.

Immagini: da un buffer a una stringa

Il lavoro di codifica "vero" più comune nel C per il web: un file binario - una JPEG, una PNG, un'icona - deve viaggiare attraverso un canale di testo, quindi diventa una stringa Base64. La ricetta è leggere il file in un buffer, dimensionare l'output con la formula, codificare, e andare avanti. La metà di lettura del file merita attenzione, perché è là che i programmi C si rompono davvero:

#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;
}

Note: rb per la lettura binaria - non negoziabile su nessuna piattaforma, perché la modalità testo può tradurre i byte e cambiare got; la coppia fseek/ftell per la dimensione (per pipe e socket senza seek, leggi in un buffer che cresce invece); e got invece di size per la codifica, perché una lettura corta è una possibilità reale. Un'immagine da 1 MB diventa circa 1,33 MB di testo - questa è la tassa, fatturata in anticipo, ed è per questo che un payload immagine base64-in-JSON dovrebbe farti fermare un attimo a chiederti se un upload di file vero non sarebbe stato più economico.

File e l'abitudine del .b64

L'altro lato del lavoro sulle immagini: devi scrivere su disco la forma Base64 di un file - un sidecar .b64, un backup di un binario in un archivio sicuro per il testo, un allegato per un mailer. Stessi conti, scrittore diverso. L'abitudine che vale la pena adottare è scrivere l'output di testo con a capo espliciti a una lunghezza che il lato ricevente si aspetta - 76 per email, 64 per consumatori di stile PEM, o nessuno se il ricevente è il tuo stesso codice:

#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;
}

Il ciclo scrive righe da 76 caratteri e una riga finale più corta; un decoder che salta gli spazi bianchi (tutti quelli seri lo fanno) non si curerà affatto della lunghezza delle righe, ed è per questo che è il ricevente da consultare, non il tuo gusto. Tieni il file di output in modalità testo (w) sul lato scrittura se vuoi gli a capo della piattaforma, o wb se il ricevente conta i caratteri in modo rigoroso - e se conta, vuole esattamente ciò che hai promesso: 76 caratteri più un a capo, nient'altro. È quella promessa, non i byte, a rendere un file .b64 un formato.

Data URI: incorporamento per davvero

Le data URI (RFC 2397) sono il lato opposto dell'arrivo preferito dell'articolo sulla decodifica: invece di ricevere data:image/png;base64,..., ne costruisci una. La forma è data:, il media type, ;base64, una virgola, il payload - e costruirlo in C è un snprintf dopo la codifica:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  /* la firma PNG da 8 byte */
  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;
}

Tre note di design. Il media type che metti nella URI è un'affermazione di cui ti assumi la responsabilità - fa prima lo sniff dei magic byte del file vero, o una data:image/png che porta una JPEG confonderà ogni consumatore a modo suo. Il flag ;base64 è obbligatorio quando il payload è Base64; omettilo e il payload deve essere testo percent-encoded, che è un formato diverso del tutto. E la guida dello stesso RFC è che le data URI sono per valori corti: incorporare inline in una pagina HTML un logo da 5 MB funziona, ma è un cattivo odore di design che un URL di risorsa vera curerebbe. La stessa costruzione compare in continuazione nelle API JSON dove un client vuole un avatar nella stessa richiesta dei dati del form - codifica, antepone, invia.

HTTP e JSON: payload che sopravvivono

La ragione moderna più grande per codificare in C è il JSON. Una stringa JSON è una sequenza di caratteri con regole di escaping, e i byte grezzi non ci stanno: un NUL nel mezzo di un letterale di stringa è un problema C, un a capo letterale dentro una stringa JSON è JSON invalido, e i byte arbitrari hanno bisogno di una storia di escaping definita. Il Base64 aggira il problema intero producendo solo caratteri che il JSON non deve mai mettere in escaping - i 64 caratteri dell'alfabeto più, nel dialetto standard, il =, e nessuno di loro è una virgoletta o una barra rovescia. Il binario entra come stringa e esce dall'altra parte esattamente come è entrato:

#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;
}

Questo stampa {"avatar": "aGVsbG8="} - un oggetto JSON completo e valido, senza alcuna meccanica di escaping, e la precisione di %.*s tiene la lunghezza esatta anche se un giorno passi a un encoder che non termina con NUL. Il costo onesto è la dimensione: ogni byte che spedisci come stringa JSON ti costa 4/3 di byte di aria più il nome del campo e le virgolette, quindi un binario da 10 KB diventa una stringa da 13,3 KB dentro il JSON. Per blob piccoli e occasionali (icone, miniature, firme, token) è un prezzo giusto; per un upload da 500 MB è un'architettura che rimpiangerai, e un upload di file vero è lo strumento per quel lavoro. Vale una riga anche questo: il + e il / dell'alfabeto standard sono sicuri dentro una stringa JSON, ma se la stessa stringa viaggia poi in una query di URL, non lo sono - quello è il lavoro della sezione URL-safe.

JWT: tre parti, un alfabeto

Il consumatore di punta del base64url in C è il JSON Web Token. Un JWT compatto secondo RFC 7519 è composto da tre parti codificate in base64url unite da punti - intestazione, payload, firma - e costruirne uno è un esercizio piacevole perché ogni pezzo è una funzione che hai già: codifica standard, traduci in URL-safe, firma, ripeti. Ecco un token HS256 costruito con l'HMAC di 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;
}

Due cose che la struttura insegna. Per primo, l'input della firma sono le due parti URL-safe unite da un punto - esattamente i byte che il ricevente vedrà - quindi la traduzione in base64url deve accadere prima della firma, non dopo; firmi la forma in alfabeto standard e la verifica del ricevente fallisce, e questo è un bug che compila, gira e ha l'aspetto di un disaccordo di chiave. Per secondo, intestazione e payload sono JSON normale in una confezione Base64: chiunque può leggerli, ed è questo il design. Un token è un biglietto firmato, non una busta sigillata - quindi non metterci dentro nulla che non ti dispiacerebbe se un utente intercettante lo leggesse, e non mettere mai, mai una password in un payload JWT "perché è codificata". La parte Base64 di questo lavoro è piccola e noiosa, ed è il complimento più alto che tu possa fare a un'implementazione JWT.

HTTP Basic Auth: costruire il token

L'intestazione di autenticazione più antica è anche il lavoro Base64 più semplice: username:password, codificato in alfabeto standard, dopo la parola Basic. Costruirla in C sono due righe, e l'unica sottigliezza è che la password può contenere i due punti (e la divisione sul lato ricevente deve essere al primo):

#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;
}

Questo stampa Authorization: Basic YWxpY2U6czNjcjN0. L'avvertimento dell'RFC vale anche sul lato mittente: questa è codifica, non protezione. Su una connessione HTTP in chiaro le credenziali sono a un solo base64 -d da chiunque sia sul filo, quindi l'auth Basic è un'abitudine solo per HTTPS. (Le alternative moderne - bearer token, mTLS - riutilizzano tutte la stessa meccanica: assembla una stringa, codificala, mettila dentro un'intestazione. Il Base64 è il modo in cui HTTP contrabbanda dati strutturati attraverso intestazioni di testo da quando il protocollo ha le intestazioni.)

Email e PEM: dove vive l'avvolgimento

L'email è la ragione per cui l'avvolgimento esiste in quanto tale. SMTP limita la lunghezza delle righe, quindi MIME ha messo un tetto di 76 caratteri alle righe codificate (il suo antenato, il PEM, di 64), e ogni sistema email onora quel tetto da trent'anni. Se il tuo programma C produce Base64 per un corpo email o un allegato, l'avvolgimento non è una cosmetica opzionale - una riga di 200 KB senza avvolgimento verrà rifiutata o rovinata da parti dell'infrastruttura email. L'encoder streaming di OpenSSL ti dà l'output avvolto gratis (alla sua lunghezza ereditata da 64 caratteri), e quando ti servono esattamente 76, avvolgere il risultato a colpo singolo è un ciclo di cinque righe:

#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;
}

Nota il \r\n: l'email vuole a capo CRLF, e se il testo codificato è uno di più parti MIME, tutto ciò che sta intorno al blocco base64 segue le stesse regole - lunghezze di riga, a capo CRLF, nessuna eccezione. I file PEM (il formato della maggior parte delle chiavi e dei certificati) usano la stessa idea con righe da 64 caratteri tra i marcatori -----BEGIN e -----END, e gli strumenti di OpenSSL si aspettano di vedere quell'armatura quando ri-salvi una chiave - quindi se il tuo programma tocca il PEM, avvolgi a 64 e conserva le etichette. Dappertutto altrove - JSON, URL, API, database - vale la regola dell'RFC e non avvolgi affatto.

Contrabbandare valori attraverso configurazioni e colonne

Un caso d'uso silenzioso: i valori che farebbero a pezzi un formato di testo vengono impacchettati come Base64 così da non farlo. Un DSN di database con i punti e virgola, una password con virgolette, un token con un a capo - il tecnico di ops li codifica una volta e il file di configurazione non vede mai i caratteri problematici:

#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;
}

Il programma stampa la riga esatta da incollare in un file .env, e il codice C che poi lo legge è una getenv più una decodifica. Tre avvertenze oneste, tutte su ciò che questa cosa non è. Non è cifratura: chiunque può leggere il file di configurazione può decodificare il valore in una chiamata, quindi non impacchettare mai un segreto come Base64 e chiamarlo protetto. Non è escaping: se il formato ha bisogno di struttura conservata, lo strumento giusto è una codifica vera (percent-encoding per le URL, escaping JSON per il JSON), e il Base64 è per i valori che quei formati non sanno esprimere - quelli binari. E costa in dimensione: un valore archiviato in una colonna TEXT di database come Base64 occupa circa il 33 percento in più di spazio rispetto all'originale, va bene per i token ed è un numero vero per le colonne di file (che è a cosa servono le colonne BLOB).

Codifica dalla shell

Prima di ricorrere a una chiamata a cc, ricorda che entrambi gli strumenti standard codificano, e lo fanno in fretta. coreutils è lo strumento generale: base64 codifica di default con avvolgimento a 76 caratteri, -w cambia la colonna, e -w 0 disattiva l'avvolgimento del tutto:

base64 photo.png > photo.b64
base64 -w 0 photo.png > photo-oneline.b64
cat note.txt | base64 -w 0

Lo strumento di OpenSSL fa lo stesso lavoro con l'imballaggio della filiera TLS: openssl base64 (l'alias amichevole di openssl enc -base64) avvolge a 64 caratteri, e -A lo passa a una riga sola:

openssl base64 < photo.png > photo.b64
openssl base64 -A < photo.png > photo-oneline.b64

Perché importa la differenza di avvolgimento? Perché se un parser a valle conta i caratteri, gli output di default dei due strumenti non sono intercambiabili - 76 per riga contro 64 per riga è una differenza visibile nel file, e un parser che toglie gli spazi bianchi non se ne cura, mentre uno che valida la lunghezza delle righe se ne cura assolutamente. Quando il tuo programma C è il produttore e la shell il consumatore (o vice versa), accordatevi prima sull'avvolgimento. Una nota sui dialetti per i sistemi di scuola BSD: il flag di decodifica lì storicamente era -D, e le release di macOS più vecchie se lo ricordano ancora. Il lato codifica è base64 dappertutto, ed è comunque l'unica direzione di cui questa sezione si occupa.

Streaming delle cose grandi

Codificare un file di diversi gigabyte in una singola malloc è un problema di memoria che non ti serviva. Il percorso streaming esiste proprio per questo, e la disciplina a blocchi di OpenSSL rende il codice quasi banale: dai a EVP_EncodeUpdate quanto il file ti dà, lascialo tenere il resto di ogni blocco di 48 byte non completo nel contesto, e scrivi i 65 byte di output di ogni blocco dritti sul file di destinazione. La memoria di picco sono i tuoi due buffer - qualche decina di kilobyte - qualunque sia la dimensione del file:

#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];            /* un multiplo di 48: righe pulite */
  unsigned char outbuf[1024 * 65 + 65];  /* 65 byte di output per blocco da 48 byte, più margine */
  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;
}

Due dettagli di design stanno facendo lavoro vero in quel ciclo. Il buffer di input è un multiplo di 48 byte, quindi ogni chiamata dà all'encoder blocchi completi e ogni riga che scrive è una riga finita da 64 caratteri; la chiamata finale poi avvolge la coda vera. Se il tuo input arriva in dimensioni arbitrarie (un socket, un disco lento), il contesto assorbe comunque il disallineamento correttamente - la scelta del multiplo di 48 riguarda la prevedibilità dell'output, non la correttezza. Il secondo dettaglio è la dimensione del buffer di output: 65 byte per ogni 48 byte di input più il margine del blocco finale (66), che è la formula avvolta della sezione sui conti applicata per blocco. Il reporting di avanzamento è una riga - conta i byte scritti su out e confrontali con il totale che hai calcolato dalla dimensione del file - e poiché la codifica ingrandisce i dati, il file di output atterrerà circa il 33 percento più grande dell'input: carica il disco in anticipo.

I bordi affilati della codifica

Le trappole, raccolte, tutte di forma C:

  • Off by one, in entrambe le direzioni. OpenSSL restituisce la lunghezza senza il suo NUL; la query sulla dimensione di Mbed TLS include lo spazio per il NUL; APR conta il NUL nelle sue lunghezze. Tre librerie, tre convenzioni di contabilità. Scrivi la funzione di dimensione del buffer una volta (la sezione sui conti) e smetti di fare aritmetica in testa sul punto di chiamata.
  • Il NUL è un byte che devi pagare. Ogni encoder di questo articolo vuole un byte in più nell'output per il terminatore, e lo vuole anche qualsiasi append al buffer che scrivi a mano. Un buffer dimensionato esattamente a encoded_chars(n) è corto di un byte nel momento in cui qualcuno vuole che printf("%s") funzioni.
  • La codifica non può fallire, quindi non devi farla andare in overflow. Non esiste un codice di errore che catturi un buffer sottodimensionato - l'encoder scriverà volentieri oltre la fine. Il modello di fallimento della codifica Base64 in C è interamente tuo: dimensiona bene, o corrompe la memoria senza nessuna diagnostica.
  • La doppia codifica è il classico bug silenzioso. Un valore che è già Base64, passato di nuovo dall'encoder, produce una stringa Base64 perfettamente valida che decodifica in una stringa Base64 invece che nei dati. Il sintomo - "decodifica, ma nella cosa sbagliata" - richiede un pomeriggio da trovare. Se un valore arriva "pre-codificato", verifica che la sua lunghezza sia un multiplo di quattro e che contenga solo caratteri dell'alfabeto prima di assumere che sia dato grezzo; se è codificato, salta la codifica.
  • Il segno più negli URL. L'output in alfabeto standard messo in una query string arriva con il + diventato spazio prima che il tuo server parseggi il form - il + è uno spazio nella codifica percent/form. Token e ID che viaggiano negli URL vogliono il dialetto URL-safe, e basta.
  • Modalità testo dal lato sbagliato della pipe. Leggere un file binario in modalità testo può tradurre i byte (su alcune piattaforme) e cambiare la tua lunghezza; scrivere Base64 avvolto con la convenzione sbagliata di a capo rompe i riceventi che contano i caratteri. rb per l'input binario, \r\n o \n espliciti dove una specifica ne esige uno, e non lasciare mai che il runtime C decida i tuoi a capo in silenzio.
  • Overflow di int nei conti sulla dimensione. ((n + 2) / 3) * 4 in aritmetica int va in overflow per input sopra i 1,5 GB circa, producendo una "dimensione richiesta" piccola e positiva e un pestaggio dell'heap. Fai i conti in size_t (o uint64_t), ed è anche per questo che l'API basata su int di APR ha un tetto di 2 GB che non puoi eliminare con l'ingegneria.
  • Avvolgimento dove il ricevente non se lo aspetta. RFC 4648 dice: nessun a capo a meno che la specifica circostante non lo chieda. Un a capo dentro il valore di una stringa JSON è invalido; dentro un URL è una richiesta diversa. Avvolgi per la posta, avvolgi per il PEM, e da nessuna altra parte.

La checklist corta

Calcola la dimensione del buffer con la formula, non con un'ipotesi, e tieni una sola funzione di dimensione per l'intera codebase. Tieni insieme (puntatore, lunghezza) anche quando il buffer è terminato con NUL, perché la lunghezza è il contratto e il NUL è una comodità. Scegli il dialetto per canale: standard per JSON e corpi, URL-safe per URL e token, avvolto per posta e armatura, non avvolto dappertutto altrove. Verifica i magic byte prima di dichiarare un MIME type in una data URI. Non usare mai il Base64 come cifratura, come sostituto del percent-encoding, o come posto dove nascondere un segreto - è una scatola, non un lucchetto. E quando i dati sono grandi, falli scorrere a stream: gli encoder a blocchi sono stati progettati esattamente per quello, e la memoria costante è l'intero punto.

Storia: come l'imballaggio è diventato standard

La storia degli encoder è la storia delle lunghezze delle righe. Il primo Base64 era un programma C dei primi anni novanta. Privacy-Enhanced Mail (RFC 1421, 1993) doveva portare il binario attraverso la posta a 7 bit, e i suoi autori scelsero sei bit per carattere in righe da 64 caratteri - il 64 è una reliquia della tolleranza di SMTP per la lunghezza delle righe, e il codice C faceva l'imballaggio con ricerche in tabella una alla volta. Quando il MIME ha standardizzato lo stesso alfabeto per il web (RFC 1521 nel 1993, RFC 2045 nel 1996), ha allentato la riga a 76 caratteri, e il mondo ha portato con sé due abitudini - 64 e 76 - che entrambe si dichiaravano "la" lunghezza di riga del Base64. Gli encoder hanno risposto: il percorso streaming di OpenSSL ha tenuto il 64 (la sua eredità PEM), lo strumento di coreutils ha scelto il 76 (la sua eredità MIME), e i due strumenti sulla stessa macchina sono ancora in disaccordo su dove mettere gli a capo. Lo standard ha preso finalmente posizione nel 2006: RFC 4648 dice che le implementazioni non devono aggiungere a capo per nulla a meno che la specifica che vi fa riferimento non lo diriga esplicitamente, ed è per questo che ogni libreria di questo articolo dà in default output non avvolto e per questo l'avvolgimento è oggi una funzione opt-in per email e armatura. L'alfabeto in sé, le regole di padding e la regola di canonicità "i bit di pad devono essere zero" risalgono a quegli RFC PEM e MIME precedenti, e 4648 li riafferma come le regole canoniche della famiglia. E la sezione 11 di quello RFC punta a un'implementazione di riferimento - un programma ISO C99, ospitato all'esterno perché il codice in sé "non poteva essere incluso in questo RFC per ragioni procedurali" - un altro promemoria che in questo formato C non è un cittadino di seconda categoria. La libreria standard di C, da parte sua, non ha mai recuperato: C89 si è congelata nel 1990, prima che qualcosa di tutto questo esistesse, e C23 nel 2024 ancora esce senza una funzione Base64. Quindi le librerie che linki sono lo standard, e la scelta tra loro è una piccola ma vera decisione di design - ed è questo di cui si è occupato questo articolo.

Piccole curiosità strane

Alcune curiosità che sono semplicemente divertenti, tutte sul lato imballaggio in C:

  • Il "64" è la base: ogni carattere di output è sei bit, e 2 alla 6 fa 64. Il formato dà nome al suo alfabeto come C dà nome ai suoi interi - per quello che il numero effettivamente è.
  • Il blocco da 48 byte dell'encoder streaming di OpenSSL non è contabilità arbitraria: 48 byte di input sono esattamente 16 gruppi di 3, e 64 caratteri di output sono esattamente 16 gruppi di 4. Entrambi i numeri sono multipli di 16, il genere di rotondità che fa felici hardware e cache line - o almeno fa felici gli umani che leggono il codice.
  • Un byte di input codifica in quattro caratteri, due dei quali sono =. Il payload non vuoto più piccolo possibile è al 50 percento padding - la codifica più sprecata del formato, e quella che ogni test suite usa perché è così facile scriverla male.
  • Mbed TLS è l'unico encoder di questo articolo a fare le ricerche in tempo costante, perché la gente che scrive crittografia embedded non si fida dell'indicizzazione di tabelle a tempo variabile nemmeno in un codec che non è una cifratura. La paranoia si trasferisce.
  • La regola della codifica canonica - i bit di pad non usati devono essere zero - suona banale finché non scopri che violarla vuol dire che due stringhe diverse possono decodificare negli stessi byte, il che rompe ogni controllo "questa stringa è la codifica di quel file?" che esista. I tuoi encoder la rispettano tutti; è per questo che il base64 è una rappresentazione stabile per hash e può fare da sostituto di un nome file in un content store.
  • EVP_EncodeBlock di OpenSSL è una delle rare funzioni C il cui valore di ritorno, il suo stesso output e il suo terminatore NUL sono tutti d'accordo: scrive n caratteri, un NUL, e restituisce n. In un linguaggio famoso per gli off-by-one, è un momento di pace.
  • APR-Util è l'unico encoder qui a chiedersi cosa significhi EBCDIC, perché Apache gira ancora su macchine dove le lettere sono in un ordine diverso da quello di ASCII. Su quelle macchine, "codificare" una stringa include prima di tutto riordinarne silenziosamente l'alfabeto.
  • L'input vuoto codifica nella stringa vuota in ogni libreria, senza pad e senza a capo. L'elemento identità del formato, presente e corretto in tutte e quattro, il che lo rende il test unitario più economico che tu scriverai mai.

Girare dalla parte del decoder

Quindi questo era il lato imballaggio: i conti, i quattro encoder, i dialetti e i posti dove i byte vanno. È la metà tranquilla del lavoro, perché la codifica non ha input invalidi e nessun decoder con cui avere disaccordi. L'altra direzione - incontrare il Base64 del mondo esterno e riavere i byte - è là dove il dolore si concentra: code con padding a zero, troncamenti silenziosi, alfabeti rigorosi contro permissivi, e una riga di comando che mangia gli a capo finali. La decodifica Base64 in C è coperta in profondità nell'articolo correlato, linkato da questa pagina, ed è il compagno naturale di questo: l'encoder scrive la scatola, il decoder la apre, e tra i due hai ogni lavoro Base64 che un programma C incontrerà mai.

Ultimo aggiornamento: 2026-09-08

Articolo correlato: Decodifica Base64 in C: una guida completa