Decodifica Base64 in C: una guida completa
Si nasconde in una risposta API, in un file di configurazione, in un allegato email o nel bel mezzo di un URL: una lunga stringa di lettere e cifre, l'occasionale + o /, e magari un = o due alla fine. La riconosci all'istante, e ora ti servono di nuovo i byte originali - in C. Ecco l'intero lavoro della decodifica Base64: dentro quattro caratteri dell'alfabeto, fuori tre byte grezzi, e via così, fino a quando i segni = non ti dicono dove finiva il dato vero. La home page di questo sito spiega il formato passo per passo, quindi questo articolo spende le energie dove c'è il lavoro vero: sui buffer, sulle librerie e sulle trappole che vivono in mezzo.
Due cose da sapere prima della prima malloc. Prima, la decodifica è la direzione che riduce: l'output fa tre quarti della dimensione dell'input, quindi un decoder non ha mai bisogno di più memoria del payload che già tiene in mano. Seconda - ed è il punto di questo articolo - C non include un decoder Base64. La libreria standard del linguaggio si è congelata molto prima che il Base64 esistesse, e nessun standard successivo ha colmato il vuoto. Perciò ogni programma C che decodifica Base64 si appoggia a una libreria, e le quattro che contano in pratica sono OpenSSL, Mbed TLS, APR-Util e GLib. Ognuna ha un carattere diverso: cosa perdona, come segnala gli errori e cosa fa in silenzio al tuo output. Appena conosci il carattere del tuo decoder, decodificare Base64 in C smette di essere una fonte di bug misteriosi e diventa una routine che sai scrivere a occhi chiusi.
La cassetta degli attrezzi: quattro modi per riavere i byte
Ecco il panorama in un colpo d'occhio. Tutte e quattro coprono l'alfabeto standard; le differenze stanno nei bordi, ed è proprio ai bordi che nascono i bug.
| Libreria | Intestazione | Modello di errore | Capriccio dell'output da ricordare |
|---|---|---|---|
| OpenSSL (libcrypto) | <openssl/evp.h> |
Restituisce -1 su input errato |
Il decoder a colpo singolo zera la coda |
| Mbed TLS | <mbedtls/base64.h> |
Codi di ritorno (-0x002C, -0x002A) |
Le regole di input più strette delle quattro |
| APR-Util | <apr-1.0/apr_base64.h> |
Nessuno: si ferma al primo carattere strano | API a lunghezza int, quindi 2 GB è il tetto |
| GLib | <glib.h> |
Restituisce NULL solo su fallimento secco |
Ignora in silenzio lo sporco che capita |
Per installare basta un nome di pacchetto per distribuzione. Per OpenSSL: libssl-dev su Debian e Ubuntu, openssl-devel su Fedora e RHEL, openssl su Arch, e brew install openssl su macOS. Per Mbed TLS: libmbedtls-dev (o mbedtls). Per APR-Util: libaprutil1-dev più libapr1-dev. Per GLib: glib2.0-dev. Poi si linka con -lcrypto, -lmbedcrypto, -laprutil-1 o -lglib-2.0, rispettivamente. Quale scegli? Se linki già OpenSSL per la TLS o per gli hash (la maggior parte dei server lo fa), usa OpenSSL. Per le build embedded e vincolate sulle risorse, Mbed TLS è il cittadino piccolo e rigoroso. Sei dentro l'ecosistema Apache? APR-Util c'è già. Se la tua codebase è basata su GNOME o GTK, GLib tiene tutto in un unico runtime.
OpenSSL: il decoder che riempie i buchi con gli zero
OpenSSL porta il Base64 in due versioni. La funzione a colpo singolo è la protagonista della maggior parte del codice:
#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;
}
Dagli un buffer di caratteri Base64 e una lunghezza, e lui scrive i byte decodificati in out e ti restituisce quanti ne sono. Toglie gli spazi iniziali, toglie gli spazi e i a capo finali, e rifiuta gli input che, dopo la pulizia, non sono un multiplo di quattro caratteri o che contengono un carattere fuori dall'alfabeto. Fin qui, un contratto perfettamente sensato. Peccato per un dettaglio che ha corrotto in silenzio più di un import di database: il valore di ritorno non è la vera lunghezza del dato.
Esegui quel programma e otterrai 4 bytes... no, aspetta. TWFuZQ== sono due gruppi di quattro caratteri, quindi la funzione restituisce 6, e il buffer contiene 4d 61 6e 65 00 00: la parola "Mane" più due byte zero. Il decoder a colpo singolo di OpenSSL lavora a quanta fissi - ogni quattro caratteri in input producono sempre esattamente tre byte in output - e quando l'ultimo gruppo portava un solo byte vero, gli altri due slot vengono riempiti con zero. Il manuale lo accenna in una frase unica e pacata ("l'output verrà riempito con bit a zero se necessario"), e quella frase è la più importante dell'intera man page di questa funzione.
La vera lunghezza si ricava dal padding, ed è un calcolo di due righe:
size_t real_length(const char *b64) {
size_t len = strlen(b64);
while (len > 0 && b64[len - 1] == '=') len--;
return len * 3 / 4;
}
Conta i caratteri dell'alfabeto, togli i pad finali, moltiplica per tre, dividi per quattro. Per TQ== (la lettera M, codificata) ottieni (2 * 3) / 4 = 1 byte vero - mentre EVP_DecodeBlock ne segnalerà tre. Tieni sempre insieme la coppia (puntatore, lunghezza), e non usare mai strlen sui dati decodificati, perché i byte che ti sono tornati possono essere una JPEG e il primo di essi può essere un NUL.
Il decoder streaming: un decoder che sa quando fermarsi
Per tutto il resto, OpenSSL offre la coppia streaming EVP_DecodeUpdate più EVP_DecodeFinal. L'oggetto contesto è ciò che sposta lo stato tra le chiamate: tiene da uno a tre caratteri di un gruppo non finito, così puoi dare il payload a pezzetti. Il comportamento che conta è questo: gli spazi bianchi (spazi, tab, carriage return, line feed) vengono saltati in qualsiasi punto dello stream, qualsiasi altro carattere non alfabetico o un = nel mezzo del dato restituiscono -1 subito, e un ritorno di 0 da un update significa "il padding è stato visto, non ci si aspetta altro". EVP_DecodeFinal poi si rifiuta con -1 se un gruppo parziale è ancora in sospeso, perché una lunghezza che non è un multiplo di quattro (dopo aver tolto gli spazi) non è un payload valido.
Una nota sulla versione prima del codice, perché i vecchi tutorial ti faranno cadere in una trappola: in OpenSSL 3.x il tipo contesto EVP_ENCODE_CTX è opaco, quindi il pattern in stack EVP_ENCODE_CTX ctx; che trovi in un sacco di codice su internet non compila più. Alloca e libera esplicitamente:
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;
}
Dimensiona il buffer di output a in_len * 3 / 4 + 3 e la chiamata è sicura per qualsiasi input. Guardalo gestire un payload MIME avvolto su righe, dove il a capo cade nel mezzo di un gruppo:
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 */
Il a capo sparisce, escono i quattro byte, e nessuno ha dovuto ripulire l'input prima. C'è una differenza in più rispetto alla funzione a colpo singolo: il percorso streaming conta i byte in modo onesto. Dagli TQ== e restituisce esattamente un byte (4d), senza padding a zero, perché capisce che due pad vuol dire che due dei tre slot di output non sono mai stati riempiti. Se al tuo payload serve mai una lunghezza affidabile da OpenSSL, questa è la strada da prendere.
Mbed TLS: quello rigoroso
Mbed TLS (la libreria crittografica nata come PolarSSL e che ora viaggia dentro gli stack embedded di ARM) ti dà due funzioni con un contratto molto pulito:
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 come farebbe una persona prudente. Chiamala con dst impostato a NULL (o dlen a zero) e ti dice la dimensione richiesta in *olen senza fare alcun lavoro; chiamala sul serio e ottieni 0 su successo, MBEDTLS_ERR_BASE64_INVALID_CHARACTER (cioè -0x002C) se c'è qualcosa di sbagliato nell'input, oppure MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL (cioè -0x002A) se la destinazione è troppo piccola. La lunghezza decodificata finisce in *olen, e a differenza della funzione a colpo singolo di OpenSSL è sempre il numero onesto: decodificare TQ== ti dà un byte, 4d, nient'altro.
Le regole di input sono le più strette delle quattro librerie, e vale la pena memorizzarle perché definiscono cosa significa "valido" per Mbed TLS:
- I a capo CRLF e LF possono comparire tra i gruppi - i payload email funzionano così come sono.
- Gli spazi sono ammessi subito prima di un a capo e alla fine del buffer, ma uno spazio dopo un a capo o nel mezzo di un gruppo è un errore.
- Al massimo due caratteri
=, e solo alla fine; qualsiasi dato dopo un pad è un errore. - Qualsiasi byte sopra 127 (accenti, frammenti UTF-8, spazzatura binaria) è un errore.
È l'ultima regola a mordere: se un payload arriva da una fonte che ha rovinato la codifica dei caratteri, Mbed TLS lo rifiuterà dove un decoder più pigro l'avrebbe decodificato con una spallata. Per tutto ciò che tocca input non fidati, il rigore è una caratteristica. Una decodifica completa si presenta così:
#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;
}
(Il fatto che la query sulla dimensione restituisca il codice "troppo piccolo" è per design: è così che la funzione comunica cosa avrebbe scritto. Entrambi i codici di ritorno di sopra vengono da <mbedtls/base64.h>, lo stesso file di intestazione in cui la funzione è dichiarata.)
APR-Util e GLib: due sedie in più
APR-Util - la libreria di utilità dell'Apache Portable Runtime, la base su cui è costruito Apache HTTP Server - porta il Base64 da quando il server deve decodificare le intestazioni di autenticazione Basic. L'API è una piccola famiglia di funzioni basate su 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);
Due cose da sapere prima di prenderselo. Prima, le lunghezze sono int: 32 bit, quindi il tetto pratico è 2 GB per chiamata, va bene per intestazioni e valori di configurazione e non va bene per decodificare un file da 4 GB. Seconda - ed è quella grossa - la funzione di decodifica non ha nessun errore in ritorno. Il comportamento si vede solo nell'implementazione, non nell'intestazione: il decoder prende qualsiasi carattere non valido, spazi bianchi e NUL compresi, come carattere terminale. Decodifica fino alla prima cosa che non riconosce, restituisce quanto ha fatto e non dice nulla. Un payload troncato, una copia-incolla con un commento in coda, un byte corrotto nel mezzo - tutto produce un output più corto, in silenzio. Se usi il decoder di APR, devi confrontare la lunghezza restituita con ciò che il payload prometteva; la funzione non lo fa al posto tuo. Non esiste un wrapper che allochi dalla pool - fornisci tu il buffer di destinazione, quindi nel codice guidato dalla pool allochi plain_dst dalla pool da solo. C'è anche un angolo EBCDIC che non trovi in nessun altro posto in questo articolo: sulle macchine EBCDIC le funzioni convertono l'input in ASCII prima di codificare e lo riportano indietro dopo la decodifica, così lo stesso codice gira sui mainframe che ancora fanno girare httpd.
GLib, il runtime dietro GTK e la maggior parte delle applicazioni GNOME, ha il carattere opposto. Il suo decoder accetta una stringa e restituisce sempre un buffer appena allocato (NULL solo se passi un puntatore NULL), decodificando tutto ciò che può e saltando in silenzio il 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);
}
La trappola è nella parola "sempre". Il decoder di GLib è della scuola permissiva: i caratteri fuori dall'alfabeto vengono saltati, non sono fatali. Dagli TWFuZ@== e ti restituirà i tre byte di "Man" senza alzare un dito. C'è anche una variante in loco comoda, g_base64_decode_inplace(), che decodifica sopra l'input buffer (sicura perché l'output è più corto dell'input) e restituisce lo stesso puntatore, così il risultato comincia all'inizio del buffer - un bel trucco per il codice a corto di memoria, e mangia volentieri l'input avvolto in CRLF. La lezione per gli sviluppatori C: se i tuoi dati non sono fidati, GLib non ti salverà da un payload corrotto. Le varianti _step (g_base64_decode_step con un intero di stato) sono disponibili quando serve una decodifica incrementale, e la coppia corrispondente g_base64_encode_step/g_base64_encode_close vive sul lato codifica.
Base64 URL-Safe: l'altro alfabeto
Da qualche parte tra l'alfabeto standard e le tue URL, qualcuno si è fatto male. Il Base64 standard usa + e / come suoi due simboli più alti, e in URL entrambi sono guai: un + in una query string viene di routine interpretato come spazio prima che il tuo server lo veda, e / è un separatore di percorso. RFC 4648, sezione 5, definisce la correzione, chiamata base64url: la stessa codifica con + sostituito da -, / sostituito da _, e il padding finale = buttato quando la lunghezza si conosce in un altro modo. I JSON Web Token, i parametri di stato OAuth e una marea di ID di sessione API vivono in questo dialetto.
Nessuna delle quattro librerie C decodifica base64url nativamente, quindi la conversione è un piccolo helper che scrivi una volta e riutilizzi: rimappa i due caratteri speciali, rimetti il padding che manca, poi passa il risultato al tuo decoder standard. Primo il controllo della lunghezza, perché una lunghezza di uno in più rispetto a un multiplo di quattro è impossibile in qualsiasi dialetto 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;
}
Due trabocchetti custodiscono questa strada. Il primo è la direzione: se dai un payload URL-safe al decoder standard senza lo scambio dei caratteri, OpenSSL e Mbed TLS lo rifiutano (quei caratteri non sono nel loro alfabeto), mentre GLib salterà in silenzio i - e i _ e ti restituirà una stringa più corta di quanto dovrebbe - senza alcun errore. Passa sempre dal helper. Il secondo è l'avvertimento dello stesso RFC, che vale la pena prendere sul serio: il base64url "non dovrebbe essere considerato identico alla codifica base64". Se un payload capita di non contenere caratteri - o _, i due dialetti sono identici byte per byte per quei dati, e lo scambio è invisibile - ed è proprio per questo che lo scambio sopravvive fino a quando non incontra un payload che ne contiene uno.
File: ripristinare l'originale
Il lavoro più comune con forma di file è il rovescio di ciò che qualche routine di esportazione ha fatto: arriva un file di testo .b64, e ti serve di nuovo il file originale. Leggi tutto il testo, decodificalo, e poi lascia che i byte si annuncino da soli prima di fidarti di qualsiasi etichetta. C non ha finfo, quindi il test pratico è una sniff dei magic number sui primi byte:
#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;
}
Note sui bordi: apri il file in modalità binaria (rb/wb) anche per la metà di testo, perché la modalità testo tradurrà i a capo su alcune piattaforme e corromperà il tuo conteggio dei caratteri; e non fare mai printf("%s") sul buffer decodificato per "vedere cos'è". La sniff dei magic number è il modo onesto di fare quella domanda, e se poi servi il file ripristinato a un browser, il Content-Type dovrebbe venire dalla stessa sniff, non dal nome del file.
Data URI: l'immagine dentro la URL
Un arrivo preferito dal mondo web: qualcuno incolla un'immagine in un form, e il front end passa al tuo server una data URI completa come data:image/png;base64,iVBORw0KGgo.... RFC 2397 definisce la forma: data:, un media type opzionale, un flag ;base64 opzionale, una virgola, e poi il payload. Quando il flag è presente, il payload è Base64; quando manca, il payload è testo normale percent-encoded - più raro, ma legale. Se il media type è omesso, il default è text/plain;charset=US-ASCII. Parserizzarlo in C significa trovare la virgola e guardare cosa c'è subito prima:
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 il chiamante si legge come una 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);
/* ora decodifica il payload con la libreria che preferisci */
}
Tre trabocchetti vivono in questo formato. Il primo è il flag ;base64 mancante: una data URI legale senza di esso porta un payload percent-encoded, e passarlo da un decoder Base64 produce spazzatura - controlla il flag, poi scegli il decoder. Il secondo è il media type dichiarato: è un suggerimento del mittente, non un fatto; la sniff dei magic number della sezione file è il tuo fatto. Il terzo è la dimensione: il consiglio dello stesso RFC è che le data URI sono per valori corti, quindi un'immagine di diversi megabyte a bordo dentro un URL è un cattivo odore nella tua architettura, non un pattern da festeggiare.
JWT: leggere le parti non segrete
Il payload Base64 più famoso del web è il JSON Web Token, e anche quello meno spaventoso una volta che ne conosci la forma. Secondo RFC 7519, un JWT compatto è tre parti base64url unite da punti: un'intestazione, un payload e una firma - ognuna codificata senza padding, senza a capo. Le prime due parti sono JSON normale, ed è per questo che tutti possono leggerle, e perché tutti dovrebbero continuare a leggere prima di toccare un token.
Leggere le prime due parti sono un paio di righe con l'helper base64url di sopra, ed è il modo più rapido per sfatare i misteri di un 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;
}
Stampata, l'intestazione è {"alg":"HS256","typ":"JWT"} e il payload è {"sub":"1234567890","name":"John Doe"}. Ora la parte che conta: la terza parte è una firma, e le due parti che hai appena decodificato non sono né segrete né autenticate. Chiunque abbia una cattura di pacchetti può leggerle, e chiunque abbia un editor di testo può riscriverle. Fidarsi del payload di un JWT in C prima di verificare la firma è il classico bug di autenticazione, e il Base64 rende facile non accorgersene - il token ha l'aspetto di un blob infrangibile ed è invece una cartolina. Per verificare un token HS256 ricalcoli l'HMAC-SHA256 su header.part con il tuo segreto usando HMAC() da <openssl/hmac.h> e confronti in tempo costante con CRYPTO_memcmp(); se i digest non coincidono, il token viene rifiutato, qualunque cosa affermi. Non esiste una libreria JWT standard de facto in C, quindi in produzione o costruisci tu quello piccolo passo di verifica o adotti una delle librerie della community - ma il lato Base64 del lavoro è la danza di divisione e decodifica di sopra, e dovresti capirla tutta.
Basic Auth: l'intestazione che non ha mai imparato la privacy
L'intestazione di autenticazione più antica del web viaggia ancora sul Base64: Authorization: Basic seguito dalla codifica in alfabeto standard di username:password (RFC 7617, che RFC 911 cita per lo schema Basic). L'RFC è esplicito: questa è codifica, non protezione - chiunque abbia una cattura di pacchetti può decodificare entrambe le metà in un solo comando - quindi il lavoro lato decodifica in C è parseggiare l'intestazione, decodificare in modo rigoroso, dividere al primo punto due (le password possono legalmente contenere punti due) e confrontare con una funzione sicura contro il 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;
}
Il controllo della lunghezza sta facendo lavoro vero: impedisce a un payload che decodifica in "alice:secret" con spazzatura finale, o a un "alice:secre" troncato, di cascare nel confronto. E CRYPTO_memcmp (o memcmp solo se capisci le implicazioni sul timing) è ciò che impedisce a un attaccante di passare in rassegna la tua lista utenti misurando i tempi. Servi questa intestazione su HTTPS o non servirla affatto - su una connessione in chiaro lo strato Base64 è solo una facciata.
Email e PEM: la casa originale
Il Base64 è nato per un problema molto specifico: il trasporto email portava solo ASCII a 7 bit, e la gente voleva spedire binari attraverso di esso. MIME (RFC 2045) ha reso il Base64 una delle codifiche di trasferimento standard e ha aggiunto due regole di casa: le righe codificate non devono superare i 76 caratteri, e il software di decodifica deve ignorare i caratteri fuori dall'alfabeto - a capo compresi. Questa seconda regola è la ragione per cui i decoder streaming di sopra masticano un allegato avvolto senza alcuna pre-elaborazione, ed è la ragione per cui l'abitudine dei 76 caratteri è ancora incastonata in ogni libreria email sulla faccia della terra. L'antenato era il PEM (Privacy Enhanced Mail, RFC 1421), che usava invece righe da 64 caratteri - la divisione 64/76 che vedi negli strumenti è proprio quella storia, entrambi i limiti imposti in ultima analisi dallo SMTP.
L'armatura PEM - il formato in cui viaggiano chiavi e certificati - è solo Base64 con un'etichetta: una riga -----BEGIN ... -----, il corpo in righe da 64 caratteri, e una riga END corrispondente. Togliere l'armatura in C è una scansione riga per riga, e poi il decoder fa il 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;
}
I byte decodificati sono DER, una serializzazione binaria compatta, ed è quello che alla fine consumano le funzioni di certificati e chiavi di OpenSSL. Due note: raccogli il corpo senza i suoi a capo (come fa il ciclo) così la tua lunghezza sarà un multiplo di quattro, e se un file porta più blocchi, fai corrispondere l'etichetta END con l'etichetta BEGIN che hai aperto - un semplice flag funziona quando ti serve solo il primo blocco, come qui.
Segreti, configurazioni e colonne di database
Il Base64 è un contenitore di testo, ed è per questo che continua a comparire in posti che non ti aspetteresti. Nei file di configurazione e nelle variabili d'ambiente è il trucco per contrabbandare valori che altrimenti farebbero a pezzi il formato: un DSN di database con punto e virgola, una password con virgolette, un valore con un a capo. Nei database, un blob binario può vivere in una colonna di testo come Base64 e sopravvivere a ogni strumento che presuppone il testo - a un costo, però, di circa un terzo in più di dimensione, quindi dimensiona le colonne di conseguenza (o chiediti perché quel valore non stia in una colonna BLOB a prescindere).
#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;
}
Il consiglio vale doppio. Prima, questa è sicurezza di formato, non segretezza: nel momento in cui uno sviluppatore può leggere il file di configurazione, può decodificare il valore in una chiamata, e la sezione di sicurezza dell'RFC registra episodi reali in cui la gente ha riferito uno scambio di protocollo al supporto e "ha rivelato per sbaglio la password" perché il Base64 maschera a livello visivo, non protegge a livello computazionale. Non archiviare mai un segreto come Base64 e chiamarlo cifrato. Secondo, valida all'avvio: un valore d'ambiente mezzo incollato è un -1 dalla chiamata rigorosa, e un controllo di una riga trasforma un fallimento criptico tre ore dopo in un messaggio azionabile all'avvio.
Decodifica dalla shell
Non tutta la decodifica avviene dentro il tuo programma. Script da CLI, job di cron e one-liner decodificano Base64 in continuazione, e gli sviluppatori C dovrebbero conoscere i due strumenti che sono già su ogni macchina Linux. Lo strumento di coreutils è quello generale: base64 -d decodifica, -i gli fa ignorare i caratteri spazzatura invece di fallire, e -w imposta la colonna di avvolgimento (che incide solo sulla codifica, non sulla decodifica):
base64 -d < blob.b64 > blob.bin
base64 -d -i < messy.b64 > blob.bin
OpenSSL ne ha uno suo, raggiungibile come openssl base64 (l'alias più amichevole di openssl enc -base64):
openssl base64 -d < blob.b64 > blob.bin
openssl base64 -d -A < blob.b64 > blob.bin
Il flag -A significa "una riga sola": codifica senza l'avvolgimento a 64 caratteri, e si aspetta che anche l'input sia una riga sola. E qui c'è una trappola CLI che ti costerà una serata se non la leggi: la decodifica base64 di OpenSSL è orientata alle righe, e un payload che arriva senza un singolo a capo decodifica in nulla, in silenzio:
printf 'TQ==' | openssl base64 -d | wc -c # 0
printf 'TQ==\n' | openssl base64 -d | wc -c # 1
Il decoder di coreutils non ha quella presunzione, ed è una delle ragioni per cui è il default più sicuro per il lavoro di incollaggio. Un'ultima nota sui dialetti: i sistemi derivati da BSD (in particolare macOS più vecchi) hanno storicamente scritto il flag di decodifica -D. Le release moderne seguono la convenzione GNU di -d, quindi controlla la man page sulla macchina su cui sei davvero.
Payload grandi, memoria piccola
La decodifica è la direzione che ti aiuta: l'output fa tre quarti della dimensione dell'input, quindi la pressione di memoria da Base64 è rara. Comunque, quando un file .b64 di centinaia di megabyte atterra su disco, il percorso streaming di prima è il tuo strumento, ed è più semplice di quanto sembri. Leggi il file codificato a blocchi, dai ogni blocco a EVP_DecodeUpdate, e scrivi i byte decodificati man mano che arrivano. Il contesto tiene da uno a tre caratteri di qualsiasi gruppo non finito tra le chiamate, quindi i confini dei blocchi possono cadere ovunque - non devi allinearli:
#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;
}
La memoria di picco è di due buffer dell'ordine di qualche decina di kilobyte, indipendentemente dalla dimensione del file, e un file corrotto fallisce in fretta - EVP_DecodeUpdate restituisce -1 nel blocco dove sta il danno, così puoi segnalare un offset invece di limitarti a una spallata. Un avvertimento di libreria per questo percorso: il decoder di APR-Util lavora su stringhe terminate da NUL con contabilità di dimensione int (il suo valore di ritorno è un int e l'input è coperto appena sotto i 3 GB da una costante interna), quindi è fuori gioco per i file multi-gigabyte. Se ti serve il reporting di avanzamento, conta i byte che hai scritto - quella è la tua posizione nell'output, e la posizione nell'input è circa quattro terzi di essa.
Le trappole, tutte specifiche del C
Raccolte in un punto solo, le trappole specifiche del fare questa cosa in C:
- La funzione a colpo singolo con padding a zero.
EVP_DecodeBlockrestituisce la lunghezza del quantum, non la lunghezza del dato.TQ==ne segnala tre ma ne porta uno. Ricalcola sempre la vera lunghezza dai pad finali, o usa la coppia streaming. - I byte decodificati non sono una stringa. Il risultato può contenere byte NUL e può non essere UTF-8. Niente
strlen, nienteprintf("%s"), niente passaggi a funzioni che presuppongono il testo. Porta (puntatore, lunghezza) ovunque. - La dimensione del buffer è un tuo compito. C non farà crescere il tuo buffer di output, e non lo faranno nemmeno i decoder - l'update di OpenSSL scrive quello che decodifica nello spazio che gli hai dato. Dimensionalo a
in_len * 3 / 4 + 3(più la quota di sovrapposizione se l'input è avvolto e decodifichi con un helper che non toglie) e tieni un controllo del limite in ogni wrapper. - Ricerche con char firmato. Se un giorno scrivi un decoder tuo, il bug classico è usare il byte di input come indice in una tabella di 256 voci con un semplice
charsu una piattaforma dove char è firmato: il byte0xFFdiventa-1e indici all'indietro nella memoria. Indica sempre con valoriunsigned charounsigned. - Quelli silenziosi sono quelli pericolosi. APR-Util si ferma al primo carattere non valido e non dice nulla. GLib salta la spazzatura e non dice nulla. OpenSSL e Mbed TLS falliscono in modo rumoroso. Se il tuo input non è fidato, il silenzio della libreria è un bug nel tuo programma, non nella libreria.
- La riga di comando mangia gli a capo.
openssl base64 -ddecodifica zero byte se l'input non ha un a capo. Pipeline shell che tolgono gli a capo finali (tr -d '\n',xargs, salvataggi dell'editor senza a capo finale) produrranno output vuoto senza errori. - int contro size_t. L'API a colpo singolo di OpenSSL prende una lunghezza
int, APR-Util usaintdappertutto, e le API di Mbed TLS e GLib usanosize_t. L'aritmetica mista di lunghezze tra loro è dove gli avvisi signed/unsigned nascondono bug veri - e dove vive il tetto dei 2 GB di APR. - Lo spazio bianco non è uniforme. OpenSSL salta tutti gli spazi bianchi ovunque. Mbed TLS permette CRLF/LF tra i gruppi e spazi subito prima di un a capo, ma non dopo uno o in mezzo a una riga. Gli strumenti CLI variano. Un payload valido per un decoder può non esserlo per un altro, e "da me girava" di solito vuol dire "il mio decoder era più pigro".
Buone abitudini, raccolte
Valida prima di fidarti: un controllo di forma (caratteri dell'alfabeto, al massimo due pad finali) cattura la spazzatura evidente prima di qualsiasi decodifica, ma solo una decodifica vera capisce la semantica del Base64, quindi al decoder rigoroso tocca l'ultima parola. Usa la coppia streaming di OpenSSL quando ti servono lunghezze oneste o input a blocchi, e la funzione a colpo singolo quando il payload è piccolo e correggi subito la sua lunghezza. Tieni insieme le coppie (puntatore, lunghezza) e non far mai incontrare un buffer decodificato con una funzione di stringa. Confronta il materiale di autenticazione con CRYPTO_memcmp. Fai la sniff dei magic byte prima di credere a un nome file o a un MIME type dichiarato. E tratta il Base64 per ciò che è - un formato di imballaggio, una piccola scatola per i byte - non come un lucchetto: nulla di queste 64 lettere rende i tuoi dati privati.
Una breve storia del Base64 in C
La storia comincia con la posta. Nel 1990 e nel 1991 un gruppo di crittografi abbozzò Privacy Enhanced Mail, un sistema per email firmate e cifrate, e aveva bisogno di un modo per portare il binario attraverso una rete a 7 bit. La loro risposta, standardizzata come RFC 1421 nel 1993, codificava i dati a sei bit per carattere - "base 64" - in righe da 64 caratteri, e l'implementazione era, ovviamente, C. Più o meno nello stesso tempo arrivò il web con il suo MIME, RFC 1521 (1993) e poi RFC 2045 (1996), che mantenne lo stesso alfabeto, allentò la lunghezza delle righe a 76 e trasformò il Base64 nel formato di allegato del giovane internet.
La libreria standard di C ha perso l'intero treno. Lo standard C89 è stato pubblicato nel 1990, tre anni prima del MIME, e il comitato del linguaggio non ha mai aggiunto una funzione Base64 da allora - né in C99, né in C11, né in C23 (la revisione del 2024). Quindi l'ecosistema è cresciuto attorno alle librerie: OpenSSL porta le routine EVP di codifica/decodifica in libcrypto da quando qualcuno linka OpenSSL per la TLS, Mbed TLS (rinominato da PolarSSL nel 2015) ha tenuto una piccola coppia rigorosa per i sistemi embedded, APR-Util è partito con Apache quando al server serviva di decodificare le sue intestazioni di autenticazione, e GLib ha aggiunto il suo trio per il desktop. Gli standard hanno inseguito le implementazioni: RFC 3548 nel 2003 ha riordinato le vecchie definizioni, e RFC 4648 nel 2006 (Base-N Encodings) ha formalizzato gli alfabeti, la variante URL-safe e le regole di sicurezza su cui si appoggia questo articolo. A proposito, la sezione 11 di quello RFC punta a un'implementazione di riferimento ISO C99 - il decoder di esempio dello standard è scritto in C, il che ti dice tutto su dove vive questo formato.
Curiosità, edizione C
Qualche curiosità con sapore di C, che fa semplicemente piacere conoscere:
- Il nome è matematica, non marketing: ogni carattere di output porta esattamente sei bit, e 2 alla 6 fa 64. "Base64" è la base, letta ad alta voce.
- L'alfabeto è di 65 caratteri, non 64: i 64 simboli più
=, che RFC 4648 chiama "il 65° carattere in più" usato per una funzione di elaborazione speciale. Il pad è un lavoratore, non una lettera. - OpenSSL avvolge l'output codificato a 64 caratteri (l'abitudine PEM) mentre coreutils avvolge a 76 (l'abitudine MIME). La differenza di 12 caratteri sono due decenni di storia della posta che puoi vedere nell'output di due comandi sulla stessa macchina.
- L'autore del comando
base64di GNU coreutils è Simon Josefsson - la stessa persona che ha scritto RFC 4648. Lo standard e una delle sue implementazioni più usate condividono un autore, ed è così che i due sono finiti per essere d'accordo su ogni caso limite. - Mbed TLS fa le ricerche in tabella attraverso helper in tempo costante (
mbedtls_ct_base64_*), quindi la velocità di decodifica non lascia trapelare quali caratteri ha visto. Un dettaglio che non noterai mai e di cui sarai contento che esista. TQ==è il payload non banale più piccolo: un byte vero, due pad. È il vettore di test perfetto - il decoder a colpo singolo di OpenSSL ne restituisce tre, il suo decoder streaming ne restituisce uno, Mbed TLS ne restituisce uno, e GLib ne restituisce uno. Quattro librerie, due risposte, e la differenza è il padding a zero.- Le funzioni base64 di APR sono le uniche in questo articolo a curarsi dell'EBCDIC, perché httpd gira ancora su macchine dove le lettere non sono ASCII. La libreria standard di C non ha mai incontrato un mainframe. APR sì.
- Il payload vuoto è l'identità universale: ogni libreria codifica e decodifica un input di lunghezza zero in un output di lunghezza zero, senza errori. Se il tuo decoder si inceppa su una stringa vuota, hai un bug, non un formato.
Girare dalla parte dell'encoder
Questa era la parte del decoder, ed è là che vive la maggior parte del dolore, perché la decodifica è dove incontri i dati degli altri: le loro scelte di padding, i loro a capo, i loro byte corrotti, i loro token. La direzione opposta - trasformare i byte in una stringa Base64 - è un animale più calmo, con il suo cast di trappole: la matematica esatta dei buffer, la questione dell'avvolgimento delle righe, e il conto della dimensione che arriva a ogni mittente. La codifica Base64 in C è coperta in profondità nell'articolo correlato, linkato da questa pagina, e si abbina a questo come un decoder si abbina a un encoder: leggi entrambi e nessuna delle due direzioni ti sorprenderà mai più.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in C: una guida completa