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++ (Cpp): una guida completa

Il problema inverso è quello con il titolo più grosso: hai dei byte - un certificato, un'immagine, un blob casuale, una firma - e devono viaggiare attraverso qualcosa che parla solo testo: un campo JSON, un'intestazione email, un URL, una variabile d'ambiente. La home page di questo sito passa in rassegna il formato in dettaglio, quindi questa è solo la versione breve: tre byte diventano quattro caratteri dell'alfabeto, una coda corta riceve uno o due segni =, e la forma codificata risulta circa il 33 percento più grande dell'originale. La codifica è la direzione che cresce, quindi ogni buffer di questo articolo è dimensionato per quello, e l'aritmetica è una riga sola - 4 * ((n + 2) / 3) - che non cambia qualunque encoder tu scelga.

Come il lato decodifica, il C++ stesso non codifica un singolo byte per te. La libreria standard ha avuto trenta anni per crescere una funzione base64 e li ha spesi tutti su altre cose, quindi ogni programma C++ porta con sé un encoder scelto da una rosa di quattro personalità molto diverse, più l'opzione di scrivere circa quaranta righe proprie. Una è un cavallo da lavoro che porta in spalla il TLS dagli anni Novanta e che fa il padding, termina in NUL e avvolge le righe senza chiedere permesso. Una è un codec veloce solo header nascosto in uno spazio dei nomi che i suoi autori hanno etichettato "detail". Una è un iteratore del 2002 che evidentemente non ha mai incontrato un carattere di padding. Una è una funzione che il sistema operativo spedisce da decenni e che aggiunge CRLF alla fine del tuo token. E la quinta opzione è la tua. Appena sai cosa ognuno aggiunge, rifiuta o aggiunge in silenzio, codificare smette di essere una fonte di bug off-by-one. Mettiamoci a impacchettare.

Lo standard non ha mai spedito un impacchettatore

Ogni standard da C++98 - e ne sono stati otto, fino al C++26 - ha guardato l'alfabeto a 64 caratteri e ha proseguito. Non c'è <base64>, non c'è std::base64, non c'è niente in <string> o <vector> che impacchetti i tuoi byte. Il lavoro tecnico del C++26 è finito ed è stato votato e approvato (114-12-3) al meeting ISO C++ di marzo 2026 a Croydon, Regno Unito, e aggiunge davvero un header <text_encoding> per il lavoro sui codec di testo; i prossimi meeting del comitato, di giugno 2026 (Brno) e novembre 2026 (Búzios, Brasile), aprono la bozza di lavoro del C++29 invece di rivedere il C++26. Il Base64 non è nello standard, ed è difficile dare la colpa al comitato: la codifica di testo riguarda gli insiemi di caratteri, e il base64 riguarda i byte, quindi il nuovo header non era mai la casa giusta. In pratica è stato l'ecosistema a fare il lavoro. Le routine base64 EVP di OpenSSL sono in ogni release OpenSSL, le librerie Boost portano due encoder indipendenti, Windows spedisce una funzione CryptoAPI con una tabella di flag per il lavoro, e uno snippet di quaranta righe viene copiato e incollato nel linguaggio dal 2008. Se il tuo progetto è basato su CMake, l'intera configurazione delle dipendenze fa tre righe:

find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)

La release Boost da tenere d'occhio è la 1.92.0, di agosto 2026, di un progetto fondato nel 1998 che spedisce librerie dalla sua prima release del 1999. Entrambi gli encoder Boost di seguito sono solo header - non c'è proprio nulla da linkare - mentre OpenSSL vuole -lcrypto, che la maggior parte dei programmi C++ che toccano il TLS ha già nel binario.

Prima, la matematica: dimensionare ogni buffer di questo articolo

Il Base64 raggruppa i byte a tre, quindi la lunghezza dell'output ha una forma che non sorprende più, una volta che la conosci: per ogni 3 byte di input, 4 caratteri in uscita, e una coda corta viene portata a padding fino a un gruppo pieno. Il conteggio esatto per n byte di input è:

4 * ((n + 2) / 3)

Il +2 è il trucco del tetto: la divisione intera arrotonda per difetto, quindi aggiungere 2 prima la fa arrotondare per eccesso al prossimo multiplo di tre. Da lì, ogni dimensione di buffer in questo articolo è una sostituzione. La funzione a colpo singolo di OpenSSL vuole un buffer che possa contenere i dati codificati più il NUL che aggiunge alla fine - la man page illustra il contratto con 16 byte di input che diventano 24 byte codificati più 1 NUL, 25 byte in totale, e la funzione restituisce la lunghezza senza il NUL. Il suo percorso in streaming elabora l'input a blocchi di 48 byte, e la man page dimensiona l'output a 65 byte per blocco (64 caratteri più l'a capo che ogni blocco produce sempre) più un byte in più per il NUL. L'header di Boost.Beast ti consegna la formula esatta come funzione constexpr. E il tuo codice riserva (n + 2) / 3 * 4, e lì è fatta. Ecco i numeri che incontrerai davvero:

Input Output (con padding) Cosa notare
1 byte 4 caratteri La forma con padding più piccola: QQ==
2 byte 4 caratteri Tre caratteri di dati e un pad
3 byte 4 caratteri Un gruppo pieno, nessun padding per niente
48 byte 64 caratteri Esattamente un blocco in streaming di OpenSSL
500 byte 668 caratteri Avvolgilo a 64 e sono 11 righe, 679 caratteri con gli a capo
1 GB circa 1,33 GB Tieni conto della colonna, del file e della rete per la tassa

Se il lato ricevente è una colonna a dimensione fissa, un buffer o una riga in un file di testo, questa formula è l'intero documento di progettazione. L'unica direzione in cui può morderti è l'altra: il lato decodifica ha bisogno di 3n/4 meno i pad, e un buffer di decodifica dimensionato con la formula di codifica è un'allocazione eccessiva classica che cresce fino a diventare un ticket di bug di memoria. Dimensionare la direzione che riduce è il problema della guida gemella; qui non fai che crescere.

Ecco il panorama, perché le differenze sono tutte negli extra - il padding, gli a capo, i NUL - e non nel pacchetto base, che ogni riga implementa in modo identico:

Encoder Da dove viene Padding Byte extra da prevedere Capriccio da ricordare
EVP_EncodeBlock <openssl/evp.h>, link -lcrypto Sempre 1 (un NUL nel buffer) L'esempio a 16 byte della man page è il contratto
EVP_EncodeUpdate + Final lo stesso Sempre 65 per blocco di 48 byte A capo fisso a 64 caratteri, ogni blocco finisce con un a capo
Boost.Beast encode boost/beast/core/detail/base64.hpp, solo header Sempre 0 Sta in uno spazio dei nomi chiamato detail
I iteratori di Boost.Serialization boost/archive/iterators/base64_from_binary.hpp, solo header Mai 0 - i 1 o 2 pad li aggiungi tu L'encoder più vecchio della cassetta degli attrezzi, 2002
CryptBinaryToStringA wincrypt.h, crypt32.lib Sempre 2 (un CRLF) se non NOCRLF Ha una flag URL-safe che il resto della cassetta degli attrezzi non ha
Le tue quaranta righe Non viene da nessuna parte: è tua Scegli tu Scegli tu Ogni caso limite è tuo per sempre

L'algoritmo base è identico in ogni riga - questa è la parte rassicurante di un formato del 1987. Ciò che differisce è cosa ogni implementazione aggiunge intorno al payload, e quasi ogni insidia di questo articolo è uno di quegli extra che incontra un consumatore che non se lo aspettava.

OpenSSL: l'encoder che il tuo stack TLS linka già

Se il tuo programma linka già OpenSSL per il TLS, non devi aggiungere nulla. La funzione a colpo singolo è una chiamata sola:

int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n);

Dammi i byte sorgente e la lunghezza, e scrive la codifica con padding su una riga. Il contratto vale la pena memorizzarlo, perché la man page lo enuncia con un esempio: per ogni 3 byte di input, 4 byte di output; una coda non divisibile per 3 viene portata a padding in modo che l'output sia sempre divisibile per 4; e in cima viene aggiunto un carattere terminatore NUL. L'esempio documentato è 16 byte in entrata, 24 byte codificati più 1 NUL, 25 byte totali nel buffer, con la funzione che restituisce 24 - la lunghezza senza il NUL. Dimensiona il buffer di conseguenza e il wrapper fa poche righe:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>

std::string openssl_encode(const std::string &in) {
  std::string out;
  out.resize(4 * ((in.size() + 2) / 3) + 1);
  int n = EVP_EncodeBlock(reinterpret_cast<unsigned char *>(out.data()),
                          reinterpret_cast<const unsigned char *>(in.data()),
                          static_cast<int>(in.size()));
  if (n < 0) return {};
  out.resize(static_cast<size_t>(n));
  return out;
}

int main() {
  std::printf("%s\n", openssl_encode("Mane").c_str());
  std::printf("%s\n", openssl_encode("M").c_str());
  std::printf("%s\n", openssl_encode("").c_str());
}

Nota cosa fa il std::string che la C ti imporrebbe: cresce fino esattamente alla lunghezza restituita, quindi il NUL aggiunto da OpenSSL è semplicemente oltre la lunghezza tracciata e non diventa mai parte del payload. Codifica "Mane" e ottieni TWFuZQ==, la classica coda di quattro caratteri con il suo singolo pad; codifica un byte e ottieni una coppia di dati su due caratteri, vestita da un costume di padding di due caratteri; non codificare niente e ottieni la stringa vuota, l'unico caso in cui un encoder base64 si comporta esattamente come la funzione identità. L'unica riga di logica vera in tutta la funzione è il resize: trasforma "byte scritti più un NUL" in "esattamente il payload".

Per i dati che arrivano a pezzi - un file, un socket, un flusso che non vuoi mettere in buffer - OpenSSL ha un contesto che alimenti e concludi, e l'aritmetica a blocchi della man page è insolitamente esplicita. Solo i blocchi pieni di 48 byte vengono elaborati immediatamente; ogni resto resta dentro il contesto e viene rilasciato da una chiamata successiva o da quella finale. Ogni blocco elaborato scrive 64 caratteri più un a capo - 65 byte - e la chiamata finale gestisce il blocco parziale, ed è per questo che il suo soffitto documentato è 65 byte più il NUL. La conseguenza da conoscere prima di chiamare: questa API avvolge a 64 caratteri. Non è configurabile. È quello che è l'encoder in streaming.

#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>

std::string openssl_encode_wrapped(const std::string &in) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_EncodeInit(ctx);
  std::string out;
  out.reserve(4 * ((in.size() + 2) / 3) + in.size() / 48 + 2);
  std::vector<unsigned char> buf(128);
  int outl = 0;
  for (size_t pos = 0; pos < in.size();) {
    size_t take = std::min<size_t>(48, in.size() - pos);
    EVP_EncodeUpdate(ctx, buf.data(), &outl,
                     reinterpret_cast<const unsigned char *>(in.data()) + pos,
                     static_cast<int>(take));
    out.append(reinterpret_cast<const char *>(buf.data()), outl);
    pos += take;
  }
  EVP_EncodeFinal(ctx, buf.data(), &outl);
  out.append(reinterpret_cast<const char *>(buf.data()), outl);
  EVP_ENCODE_CTX_free(ctx);
  return out;
}

int main() {
  std::string s = openssl_encode_wrapped(std::string(500, 'A'));
  std::printf("500 bytes -> %zu chars\n", s.size());
  int lines = 0;
  size_t longest = 0, run = 0;
  for (char c : s) {
    if (c == '\n') { lines++; run = 0; }
    else run++;
    longest = std::max(longest, run);
  }
  std::printf("lines=%d longest=%zu lastchar=%c\n", lines, longest, s.back());
}

Alimentalo con 500 byte della lettera A e i conti tornano esattamente come la man page aveva promesso: 668 caratteri codificati, e poiché l'output è tagliato in righe da 64 caratteri, ottieni 11 righe, 679 caratteri in tutto, e l'ultimo carattere in assoluto è un a capo. Quello a capo finale è quello che rompe i consumatori: incolla il risultato in una stringa JSON e hai un carattere di controllo dove doveva esserci una virgoletta; usalo come segmento di token e hai inventato un nuovo segmento. La regola pratica: l'API a blocchi per i payload su una riga (token, header, valori di configurazione), l'API in streaming quando il consumatore vuole un output avvolto in forma MIME, e quando hai dubbi, toglie l'a capo finale con un ciclo while (out.back() == '\n') prima che il payload attraversi un confine che non se lo aspetta.

Boost.Beast: un impacchettatore veloce in uno spazio dei nomi detail::

La libreria HTTP di Boost spedisce un codec base64 all'improbabile indirizzo boost/beast/core/detail/base64.hpp. Lo spazio dei nomi detail:: è il modo di Boost per dire "questa è roba nostra", e i manutentori si sono rifiutati di promuovere il codec a API pubblica. Tutti lo usano lo stesso: è piccolo, è veloce, è solo header (definisce BOOST_BEAST_HEADER_ONLY prima dell'include e non c'è nulla da linkare), ed è lo stesso codec che la negoziazione WebSocket di Boost.Beast usa per il calcolo di Sec-WebSocket-Accept, il che significa che mastica traffico vero da anni.

Sul lato codifica l'API è quasi insultantemente calma. Un helper constexpr ti dà la dimensione esatta dell'output - 4 * ((n + 2) / 3), la stessa formula della sezione matematica, ora con un compilatore a verificarla - e la funzione encode scrive il risultato con padding nel tuo buffer e ti dice quanti caratteri ha usato. Non c'è un canale di errore, perché la codifica non può fallire: qualsiasi byte è un input valido, e la lunghezza dell'output è una funzione pura della lunghezza dell'input. Il wrapper:

#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace b64 = boost::beast::detail::base64;

std::string beast_encode(const std::string &in) {
  std::string out(b64::encoded_size(in.size()), '\0');
  std::size_t n = b64::encode(out.data(), in.data(), in.size());
  out.resize(n);
  return out;
}

int main() {
  std::printf("%s\n", beast_encode("Mane").c_str());
  std::printf("%s\n", beast_encode("M").c_str());
}

Codifica "Mane" e ottieni TWFuZQ==; codifica il singolo byte M e ottieni TQ== - gli stessi byte prodotti dal wrapper OpenSSL, senza NUL di cui preoccuparsi e senza righe da togliere. Due cose da mettere da parte. Per primo, la provenienza: il sorgente ha un copyright 2016-2019 di Vinnie Falco, con un piè di pagina che attribuisce alcune parti a uno snippet di Rene Nyffenegger del 2004-2008 - la stessa canzone popolare che ha iniziato la storia del base64 in C++, ora spedita dentro Boost, nel tuo binario, a fare negoziazioni WebSocket per tutta la rete. Per secondo, quella pratica: poiché il codec fa il padding e non avvolge mai, è lo strumento giusto per qualsiasi cosa debba stare su una riga - token, header, payload API - e la formula di encoded_size ti dà un buffer esattamente giusto, mai un'approssimazione.

Boost.Serialization: l'iteratore che ha dimenticato che il padding esista

Il base64 più vecchio dell'ecosistema C++ non è una funzione, ma un insieme di adattatori di iteratori componibili, scritti da Robert Ramey nel 2002 per la libreria di serializzazione di Boost. La direzione di codifica è una catena di due adattatori: un trasformatore di larghezza che raggruppa di nuovo i tuoi byte grezzi da otto a sei, e un iteratore che trasforma ogni valore rigruppato in un carattere dell'alfabeto:

#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <cstdio>
#include <string>

namespace it = boost::archive::iterators;

std::string boost_iter_encode(const std::string &in) {
  using enc =
      it::base64_from_binary<it::transform_width<const char *, 6, 8>>;
  std::string out(enc(in.data()), enc(in.data() + in.size()));
  switch (in.size() % 3) {
    case 1: out += "=="; break;
    case 2: out += '=';  break;
    default: break;
  }
  return out;
}

int main() {
  std::printf("%s\n", boost_iter_encode("Mane").c_str());
  std::printf("%s\n", boost_iter_encode("M").c_str());
}

L'iteratore fa il pacchetto base e nient'altro - nessun padding, nessun NUL, nessun a capo, e nessun canale di errore, perché il pacchetto base non può fallire. Codifica "Mane" e l'iteratore ti consegna sei caratteri, TWFuZQ, senza battere ciglio: una vera codifica di quattro byte è otto caratteri, e a un iteratore del 2002 non era mai venuto in mente di curarsene. È per questo che la dichiarazione switch è portante, non decorativa: un byte in meno rispetto a un gruppo riceve due pad, due byte in meno ne riceve uno. La stessa catena senza la switch è ciò che ottieni se dimentichi quel passo, e il risultato è una stringa che si decodifica bene con un decoder tollerante, fallisce con uno rigoroso, e trasforma il messaggio di errore del tuo consumatore API in un mistero. (Il lato decodifica di questa stessa famiglia di iteratori è quello che lancia un'eccezione su un singolo spazio fuori posto - di più nella guida gemella.)

Quaranta righe, zero dipendenze

Il Base64 è abbastanza piccolo da rendere un encoder corretto una cosa rispettabile da possedere, e in C++ il rendimento è migliore che in qualsiasi altro linguaggio: std::string rende piacevole la gestione dei buffer, la formula ti dà la dimensione esatta in partenza, e un encoder fatto a mano è quello che non ha opinioni di nessun tipo - nessun NUL, nessun a capo, nessuna abitudine di piattaforma - che è esattamente ciò che vuoi sotto un file di configurazione o un confine API. Questa versione impacchetta a gruppi di 3 byte contro una tabella di 64 caratteri:

#include <cstddef>
#include <cstdio>
#include <string>

std::string base64_encode(const std::string &in) {
  static const char *table =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  std::string out;
  out.reserve((in.size() + 2) / 3 * 4);
  const unsigned char *p =
      reinterpret_cast<const unsigned char *>(in.data());
  size_t n = in.size();
  for (size_t i = 0; i < n; i += 3) {
    unsigned v = p[i] << 16;
    if (i + 1 < n) v |= p[i + 1] << 8;
    if (i + 2 < n) v |= p[i + 2];
    out.push_back(table[(v >> 18) & 63]);
    out.push_back(table[(v >> 12) & 63]);
    out.push_back(i + 1 < n ? table[(v >> 6) & 63] : '=');
    out.push_back(i + 2 < n ? table[v & 63] : '=');
  }
  return out;
}

int main() {
  std::printf("%s\n", base64_encode("Mane").c_str());
  std::printf("%s\n", base64_encode("M").c_str());
  std::printf("%s\n", base64_encode("M\312\277").c_str());
}

Percorri le parti. La riga reserve è la sezione matematica: (n + 2) / 3 * 4 caratteri, esattamente, quindi non c'è riassegnazione a metà del ciclo. Il reinterpret_cast in const unsigned char * non è una formalità - su piattaforme dove char è firmato, un byte sopra 127 sarebbe altrimenti un numero negativo, e nel momento in cui toccasse un indice di tabella avresti comportamento indefinito, completo di camice da laboratorio. Ogni iterazione tira fino a tre byte in un valore a 24 bit, spinge le quattro fette a 6 bit nella tabella, e in coda emette = al posto di qualunque byte non ci fosse - le guardie i + 1 < n e i + 2 < n sono l'intera logica di padding. Dargli "Mane" e ottieni TWFuZQ==. Dargli un singolo M e ottieni TQ==. Dargli byte sopra 127 - la coppia 0xCA 0xBF della terza riga dell'esempio - e l'output resta ASCII puro (Tcq/), perché un byte sopra 127 è solo un byte, e alla tabella non importa cosa significhi. Quaranta righe, nessuna dipendenza, e ogni caso limite è una riga che hai scritto tu, che è l'intero punto.

Windows CryptoAPI: l'impacchettatore integrato nel sistema operativo

Su Windows c'è un encoder base64 nel sistema operativo stesso, più vecchio della maggior parte dei framework di questo articolo: CryptBinaryToStringA da wincrypt.h, in crypt32.lib, parte della CryptoAPI che spedisce con Windows da decenni. Converte un array di byte in una stringa formattata, e la sua tabella di flag si legge come un menu dell'intera storia del formato:

Flag Valore Cosa ottieni
CRYPT_STRING_BASE64HEADER 0x0 Base64 avvolto nelle righe di header BEGIN/END del certificato
CRYPT_STRING_BASE64 0x1 Base64 semplice, senza header
CRYPT_STRING_BASE64URI 0xD L'alfabeto URL-safe: + diventa -, / diventa _, secondo RFC 4648 sezione 5
CRYPT_STRING_NOCRLF 0x40000000 Nessun a capo aggiunto alla fine
CRYPT_STRING_NOCR 0x80000000 Un solo LF al posto del CRLF predefinito

La prima cosa da sapere è il comportamento predefinito: a meno che non passi CRYPT_STRING_NOCRLF, la funzione aggiunge una coppia CR/LF alla fine della tua stringa - il comportamento documentato è che ogni formato non binario riceve una sequenza di a capo - quindi un token base64 che deve stare su una riga vuole BASE64 | NOCRLF, e quella combinazione è la chiamata idiomatica. La seconda cosa è la convenzione di chiamata, che è il classico doppio passo di Windows: chiama con un buffer NULL per chiedere quanto spazio serve (la risposta include il NUL terminale), alloca, chiama di nuovo, e leggi la lunghezza senza il NUL:

#include <windows.h>
#include <wincrypt.h>
#include <cstddef>
#include <string>

std::string win32_encode(const std::string &in,
                         DWORD flags = CRYPT_STRING_BASE64) {
  DWORD need = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            nullptr, &need))
    return {};
  std::string out(need, '\0');
  DWORD got = 0;
  if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
                            static_cast<DWORD>(in.size()),
                            flags | CRYPT_STRING_NOCRLF,
                            out.data(), &got))
    return {};
  out.resize(got);
  return out;
}

Due note ancora. La flag URI è l'unico base64url nativo in questo intero articolo - su Windows puoi codificare direttamente l'alfabeto dei token, e l'approccio della trascodifica nella sezione qui sotto è rigorosamente per le altre piattaforme. E la voce CRYPT_STRING_BASE64HEADER, con il suo valore 0, è anche la flag che ottieni se passi zero, quindi una chiamata che "voleva" dire nessuna flag per niente avvolge in silenzio il payload nelle righe di header del certificato - l'abitudine dell'era PEM di mettere in cornice, utile per generare file .pem e una sorpresa per tutto il resto. Linka contro crypt32.lib e la funzione è tua per il resto della vita del programma.

Base64url: l'alfabeto per token e URL

L'alfabeto standard ha due caratteri che non sopravvivono a un URL: + significa spazio in una stringa di query, e / significa directory in un percorso. RFC 4648 sezione 5 risolve il problema con due scambi di caratteri - + diventa - e / diventa _ - ed è schietto sul risultato: questa codifica "non dovrebbe essere considerata la stessa della codifica base64". È l'alfabeto dei JWT, delle code challenge di OAuth PKCE, degli identificatori video di YouTube e della maggior parte dei token API, e di solito toglie anche il padding =, perché in un token la lunghezza è nota implicitamente e i pad sarebbero solo percent-escape in attesa di succedere.

Dei encoder di questo articolo, solo la flag di Windows emette l'alfabeto nativamente - OpenSSL non ha una modalità URL-safe, e nessuna delle varianti Boost ce l'ha - quindi sulla maggior parte delle piattaforme la ricetta è: codifica standard, scambia i due caratteri, togli i pad. Sono una dozzina di righe:

#include <cstddef>
#include <cstdio>
#include <string>

/* base64_encode dalla sezione "Quaranta righe, zero dipendenze" */

std::string base64url_encode(const std::string &in, bool pad = false) {
  std::string out = base64_encode(in);
  for (char &c : out) {
    if (c == '+') c = '-';
    else if (c == '/') c = '_';
  }
  if (!pad)
    while (!out.empty() && out.back() == '=')
      out.pop_back();
  return out;
}

int main() {
  std::printf("%s\n", base64url_encode("M\312\277").c_str());
  std::printf("%s\n", base64url_encode("M").c_str());
  std::printf("%s\n", base64url_encode("M", true).c_str());
}

La prima riga di output è Tcq_, dove l'alfabeto standard avrebbe scritto /; le altre due righe mostrano l'interruttore dei pad in azione - TQ senza padding di default, TQ== quando il consumatore li vuole indietro. Quello argomento pad è quello su cui vale la pena riflettere, perché i consumatori non sono d'accordo: i segmenti JWT non vogliono pad, le challenge PKCE non vogliono pad, ma un valore base64url che finisce in un campo dove il decoder è rigoroso sulla lunghezza potrebbe volerli indietro, e l'interruttore è un bool, non una riscrittura. E la modalità di fallimento da ricordare nella direzione opposta: un - dentro un payload con alfabeto standard è semplicemente non valido, quindi i due alfabeti non sono scambiabili a livello di byte - un token coniato con l'alfabeto sbagliato non si decodifica, fallisce, ed è il fallimento che vuoi a un confine di sicurezza.

A capo delle righe: 64, 76 o mai

Il base64 avvolto ha tre lunghezze di riga nel mondo reale, ciascuna con una storia. L'encoder in streaming di OpenSSL è fermo a 64 caratteri - l'abitudine PEM, dove lo standard del 1987 per Privacy-Enhanced Mail avvolgeva a 64. MIME, quando ha standardizzato la codifica per l'email nel 1993, è passato a 76 caratteri, e quel numero è il predefinito del comando base64 di coreutils (la sua flag -w imposta la larghezza, e -w 0 spegne del tutto l'avvolgimento) e della maggior parte degli strumenti dell'ecosistema. RFC 4648 stesso non prende parti: cita 76 come limite di MIME e dice alle implementazioni di non avvolgere per niente a meno che la specifica che le richiama non glielo indichi. Quale emettere dipende da chi lo consuma, ed è il consumatore - non il formato - a essere il vincolo di progettazione.

L'avvolgimento è un passo di post-elaborazione sulla stringa codificata, mai un passo di input: i gruppi di 4 caratteri sono l'unità di significato, quindi tagliare la stringa a qualsiasi multiplo della larghezza è un taglio sicuro - ogni confine di riga cade tra gruppi. La versione C++ è un ciclo:

#include <cstddef>
#include <cstdio>
#include <string>

/* base64_encode dalla sezione "Quaranta righe, zero dipendenze" */

std::string wrap_lines(std::string s, size_t width = 76) {
  std::string out;
  for (size_t i = 0; i < s.size(); i += width)
    out += s.substr(i, width) + "\r\n";
  return out;
}

int main() {
  std::string mime = wrap_lines(base64_encode(std::string(200, 'x')));
  int lines = 0;
  for (char c : mime)
    if (c == '\n') lines++;
  std::printf("mime: %d lines, %zu chars\n", lines, mime.size());
}

I conti: 200 byte si codificano in 268 caratteri, e avvolto a 76 con terminatori CRLF sono 4 righe - tre righe piene e una coda di 40 caratteri - 276 caratteri sulla rete. La scelta CRLF nello snippet è la scelta dell'email; per tutto il resto, LF è il predefinito moderno, e l'unica regola non negoziabile è la coerenza - un decoder che si aspetta CRLF leggerà un solo LF come carattere di dati se è rigoroso. (La regola di MIME è che i decoder devono ignorare gli a capo, ed è per questo che l'email non ha mai sofferto la differenza.) La terza abitudine da conoscere: il comando openssl base64 - che è il programma enc con un trench addosso, che controlla il proprio nome in argv[0] - avvolge a 64 senza -A ed emette una riga con -A, ed è l'unico strumento della riga di comando il cui comportamento controlli a ogni esecuzione invece di fidarti della memoria.

Binario in JSON e nelle configurazioni

Una stringa JSON ha una piccola lista di caratteri che non può contenere in forma grezza: la virgoletta, la barra invertita, e i caratteri di controllo sotto 0x20. Un certificato, una chiave casuale, una firma - tutti sono pieni di byte che diventerebbero una cascata di escape se provassero a stare dentro un campo stringa grezzo, e i caratteri di controllo farebbero strozzare alcuni parser direttamente. Il Base64 è la soluzione, ed è la risposta predefinita che ogni formato di configurazione che deve trasportare binario ti dà: il valore viene memorizzato come una riga singola di caratteri alfabeti puri, e le regole di citazione della libreria JSON non hanno più nulla da fare.

Lo schema C++ è l'intera implementazione: leggi i byte (in modalità binaria, ovviamente), codifica, memorizza la stringa. Il consumatore decodifica dall'altra parte. La trappola specifica di JSON è la stringa avvolta: un certificato avvolto a 76 caratteri incollato dritto in un file JSON è una stringa piena di caratteri di controllo letterali, che è o un errore di parsing o una corruzione silenziosa, a seconda dell'umore del parser. Se il valore deve essere avvolto per gli occhi umani, deve essere escapato o deve stare su una riga - e per configurazioni macchina-a-macchina, una riga è la risposta. L'altra trappola è il valore senza etichetta: una colonna di configurazione che dice base64 in una man page del 2014 di solito è alfabeto standard con padding, ma i token dell'era API sono URL-safe senza padding, e il test a quattro caratteri della guida di decodifica - contiene + o /, - o _, un = alla fine? - è l'intera diagnosi.

Data URI: file che si incollano nelle pagine

Una data URI è un URL il cui payload è proprio lì nell'indirizzo: data:, un tipo media facoltativo, un marcatore ;base64 facoltativo, una virgola, e i dati stessi - l'intero schema di RFC 2397. I browser li usano per incorporare immagini, font e piccoli script direttamente in HTML e CSS senza richieste extra, e se una pagina continua a funzionare con la rete disattivata, una data URI è una forte sospettata. Dal lato C++, il lavoro di codifica è assemblare la stringa, che è una concatenazione di stringhe con una costante:

#include <cstdio>
#include <string>

/* base64_encode dalla sezione "Quaranta righe, zero dipendenze" */

std::string make_data_uri(const std::string &mime_type,
                          const std::string &binary) {
  return "data:" + mime_type + ";base64," + base64_encode(binary);
}

int main() {
  std::printf("%s\n", make_data_uri("text/plain", "hi").c_str());
}

Le insidie sono tutte nei dettagli. Il marcatore ;base64 fa esattamente sette caratteri, che è la lunghezza che i bug off-by-one prendono di mira: un parser che controlla sei è un parser che accetta data:text/plain;base4,... e decodifica spazzatura senza battere ciglio. E il payload di una data URI base64 è una riga - gli a capo non fanno parte della grammatica della URI, quindi se il tuo encoder ha avvolto l'immagine a 76 (e gli encoder in forma MIME lo fanno, di default), la URI è rotta prima di arrivare al browser. La regola per questo consumatore: codifica, non avvolgere, e tieni il tipo media accurato - un image/png sbagliato su un JPEG è il genere di bugia che emerge solo come un'anteprima rotta alle 2 di notte.

Token: JWT, PKCE e chiavi API

Il base64 con la posta in gioco più alta su internet sta in un token. Un JSON Web Token è tre segmenti base64url incollati con dei punti: un JSON header, un JSON claims, e una firma calcolata sulla stringa header.claims. Il C++ non ha un tipo JWT integrato, ma costruirne uno è l'encoder base64url di qui sopra più una chiamata HMAC, perché l'intero token è base64url finché non lo è più - finché non diventa una firma:

#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>
#include <openssl/hmac.h>

/* base64_encode e base64url_encode dalle sezioni precedenti */

std::string jwt_hmac256(const std::string &signing_input,
                        const std::string &secret) {
  unsigned char digest[EVP_MAX_MD_SIZE];
  unsigned int len = 0;
  HMAC(EVP_sha256(), secret.data(), static_cast<int>(secret.size()),
       reinterpret_cast<const unsigned char *>(signing_input.data()),
       signing_input.size(), digest, &len);
  return std::string(reinterpret_cast<const char *>(digest), len);
}

int main() {
  const std::string header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  const std::string claims_json =
      "{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";
  std::string head = base64url_encode(header_json);
  std::string claims = base64url_encode(claims_json);
  std::string signing_input = head + "." + claims;
  std::string sig = base64url_encode(jwt_hmac256(signing_input, "secret"));
  std::printf("token: %s\n", (signing_input + "." + sig).c_str());
}

Esegui l'esempio e il token che ne esce è un token HS256 da manuale: l'header si decodifica in {"alg":"HS256","typ":"JWT"}, i claims in un soggetto, un nome e un timestamp di emissione, e la firma è il base64url di un HMAC-SHA256 sui due segmenti codificati. Tre dettagli reggono l'intero design. L'input di firma sono i segmenti codificati, non il JSON grezzo - firmi il JSON e hai firmato i byte sbagliati. I segmenti sono base64url senza padding - i pad starebbero in mezzo a un URL, e il punto dell'alfabeto era tenere il token come una stringa pulita. E HS256 significa un segreto condiviso, che è un algoritmo server-a-server: un segreto che vive nel codice di un client non è un segreto, e il token che firma non è una credenziale. (Il flusso PKCE di OAuth usa lo stesso alfabeto a un passo di distanza: un verifier casuale, trasformato in hash con SHA-256, messo in base64url senza pad in una code challenge - l'encoder della sezione base64url è l'intera implementazione lato client.)

HTTP Basic Auth

Il base64 più vecchio in HTTP è l'header delle credenziali: Authorization: Basic seguito dal base64 di user:password, uno schema così vecchio che precede il JSON. Costruirlo è una concatenazione:

#include <cstdio>
#include <string>

/* base64_encode dalla sezione "Quaranta righe, zero dipendenze" */

std::string basic_auth_header(const std::string &user,
                              const std::string &pass) {
  return "Basic " + base64_encode(user + ":" + pass);
}

int main() {
  std::printf("%s\n", basic_auth_header("user", "password").c_str());
}

L'output è la stringa che probabilmente hai visto in una richiesta intercettata: Basic dXNlcjpwYXNzd29yZA==. Due note C++. La concatenazione user + ":" + pass è il punto in cui una password contenente due punti confonderebbe un parser ingenuo dall'altra parte - la regola di parsing è "dividi al primo punto", ed è per questo che il lato costruzione è libero di mettere qualsiasi cosa in entrambi i campi. E se le credenziali non sono ASCII, la lettura sicura dello schema è trattare l'identificativo utente e la password come UTF-8 prima del base64, che in C++ significa che il tuo std::string sta già facendo il lavoro - a patto che l'abbia riempito con byte UTF-8 e non con ciò che la località ha deciso. La nota di sicurezza va con ogni menzione di questo schema: l'autenticazione Basic è offuscamento, non protezione. L'header viaggia in chiaro per chiunque possa leggere la rete, quindi è accettabile solo dietro TLS, e anche così è la scelta per chiamate macchina-a-macchina, non per le persone. (Il codec di Boost.Beast - quello dello spazio dei nomi detail:: - fa questo stesso lavoro di header dentro l'implementazione WebSocket di Boost, mettendone in base64 il digest SHA-1 che diventa la chiave Sec-WebSocket-Accept, che è la prova silenziosa che lo schema lo fa dal 2017.)

Email: regole a sette bit, risposta base64

L'email è il luogo dove il base64 ha imparato le sue abitudini, e le abitudini sono ancora portanti. SMTP, nella sua forma originale, è stato costruito per trasportare ASCII a sette bit, quindi qualunque cosa binaria doveva essere riscritta come testo stampabile prima di poter viaggiare. Privacy-Enhanced Mail l'ha fatto nel 1987 con righe da 64 caratteri e un controllo di integrità del messaggio RSA-MD2/MD5 incollato alla fine, e MIME, quando ha standardizzato la codifica per l'email nel 1993, ha rilassato il limite a 76 caratteri e ha aggiunto la regola per cui un decoder conforme deve semplicemente ignorare gli a capo. Un allegato email è ancora base64 oggi, avvolto a 76, e l'aritmetica esatta arriva a 4/3 per 78/76 - circa il 137 percento della dimensione originale, più circa 814 byte di header.

Il lato C++ è l'encoder più la funzione di avvolgimento di qui sopra - codifica su una riga, avvolgi a 76 con CRLF, fatto. I due dettagli specifici dell'email: l'ultima riga può o non può portare un a capo finale (i decoder sono tenuti a ignorarlo, quindi entrambi i casi sono legali e entrambi comuni), e il valore avvolto non è un valore JSON, una variabile d'ambiente o un token - è un blob che appartiene a un corpo MIME, e spostarlo altrove è dove l'avvolgimento smette di essere un'abitudine e diventa un bug. La direzione opposta - un allegato che arriva avvolto a 76 - è il territorio della guida gemella, dove i quattro decoder C++ non sono d'accordo sugli a capo in quattro modi diversi.

File, flussi e il soffitto dei due gigabyte

Codificare un file è lo specchio del lavoro sui file della guida di decodifica: apri in modalità binaria (su Windows una lettura in modalità testo tradurrebbe le coppie CRLF in singoli a capo e cambierebbe i tuoi dati prima che l'encoder li veda), leggi i byte, codifica, scrivi in binario. La versione per file piccoli è un lavoro di una funzione:

#include <cstdio>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>

/* base64_encode dalla sezione "Quaranta righe, zero dipendenze" */

std::string encode_file(const std::string &path) {
  std::ifstream in(path, std::ios::binary);
  if (!in) return {};
  std::vector<unsigned char> bytes{std::istreambuf_iterator<char>(in),
                                   std::istreambuf_iterator<char>()};
  return base64_encode(
      std::string(reinterpret_cast<const char *>(bytes.data()), bytes.size()));
}

int main() {
  std::string b64 = encode_file("/etc/hostname");
  std::printf("file -> %zu chars\n", b64.size());
}

Il soffitto è il fatto specifico di C++ nel titolo della sezione: ogni parametro di lunghezza nell'API EVP è un int. Una singola chiamata a EVP_EncodeBlock può quindi codificare al massimo circa 2 GB di input, e il buffer di output per quella chiamata - 1,33 volte più grande - non sta in un int nemmeno per sbaglio. Sotto il soffitto, l'API a blocchi va bene per i file che stanno in memoria. Sopra di esso, o per un file che non vuoi in memoria, vai a frammenti - e la regola dei frammenti è l'unico vincolo specifico di base64 sul ciclo: i frammenti devono essere multipli di 3 byte, perché il raggruppamento è a tre e un confine di frammento in mezzo a un gruppo cambia l'output. 3072 - tre frammenti da 1024 byte - è una dimensione di frammento comoda, e il ciclo diventa:

#include <algorithm>
#include <cstddef>
#include <string>

/* base64_encode dalla sezione "Quaranta righe, zero dipendenze" */

std::string encode_streamed(const std::string &data) {
  std::string out;
  for (size_t pos = 0; pos < data.size();) {
    size_t take = std::min<size_t>(3072, data.size() - pos);
    out += base64_encode(data.substr(pos, take));
    pos += take;
  }
  return out;
}

Ogni frammento si codifica in modo indipendente e la concatenazione è identica al risultato a colpo singolo - questa è la proprietà che rende i frammenti sicuri in primo luogo, e cade direttamente dal raggruppamento a 3 byte. (Il contesto in streaming di OpenSSL della sezione encoder fa lo stesso lavoro aggiungendo gratuitamente l'a capo delle righe a 64 caratteri, che è lo strumento giusto quando il consumatore vuole la forma MIME.) E il lato output ha lo stesso budget del lato input: un file da 10 GB diventa una stringa da 13,3 GB, quindi il buffer - o il file che stai scrivendo - è dimensionato con la formula della sezione matematica, e il soffitto di int dice che il percorso a frammenti non è una comodità sopra i 2 GB - è l'unico percorso.

Variabili d'ambiente e riga di comando

Le variabili d'ambiente hanno lo stesso problema delle stringhe JSON e una risposta peggiore: non possono trasportare byte NUL per niente, e i caratteri di controllo non sono i loro amici. Il trucco standard è mettere in base64 il payload perché sopravviva alla shell, e in C++ la direzione di codifica è una riga sola:

#include <cstdio>
#include <cstdlib>
#include <string>

/* base64_encode dalla sezione "Quaranta righe, zero dipendenze" */

int main() {
  setenv("MY_PAYLOAD", base64_encode("hello, env").c_str(), 1);
  std::printf("env: %s\n", getenv("MY_PAYLOAD"));
}

Il valore che atterra nell'ambiente è aGVsbG8sIGVudg==: alfabeto puro, sicuro per la shell, sicuro per un file .env, sicuro per un dashboard CI, e decodificabile su qualsiasi macchina che abbia un decoder base64. La riga di comando stessa ha la stessa storia a due strumenti del lato decodifica, con le flag della direzione di codifica: base64 di coreutils (o la reimplementazione uutils che le distribuzioni più nuove spediscono; controlla con base64 --version) avvolge a 76 di default, e -w 0 ti dà una riga; openssl base64 - il programma enc che controlla il proprio nome in argv[0] e si mette in modalità base64 - avvolge a 64 e accetta -A per una riga singola:

# una riga, per token e configurazione
base64 -w 0 < payload.bin > payload.b64
openssl base64 -A < payload.bin > payload.b64

# avvolto, per email e file di testo
base64 < payload.bin > payload-76.b64
openssl base64 < payload.bin > payload-64.b64

Nessuno dei due parla base64url nativamente, quindi un token che coni in una shell riceve il trattamento della trascodifica prima di entrare in un URL. E la riga di comando è il luogo dove l'abitudine al fallimento silenzioso del lato codifica è più pericolosa: un encoder che avvolge quando il tuo consumatore si aspetta una riga non darà errore, produrrà semplicemente una stringa con a capo dentro - che è esattamente il fallimento che ora stai inseguendo in produzione. Per qualsiasi cosa conti, codifica nel tuo programma, dove il buffer è dimensionato dalla formula e la forma delle righe è una variabile che controlli tu.

Insidie: edizione C++

  • Il NUL che non hai ordinato. EVP_EncodeBlock aggiunge un terminatore NUL dopo il payload. L'esempio della man page: 16 byte in entrata, 24 codificati più il NUL, 25 nel buffer, 24 restituiti. Dimensiona per il byte extra e ridimensiona al valore restituito, oppure il tuo token finisce con un byte a zero.
  • Il 64 fisso. L'API in streaming di OpenSSL avvolge a 64 caratteri, ogni blocco finisce con un a capo, e non c'è una flag per cambiarlo. L'output avvolto di un encoder in un consumatore su una riga è un bug da carattere di controllo.
  • Il blocco di 48 byte. EVP_EncodeUpdate emette output solo per blocchi di input pieni di 48 byte; il resto sta nel contesto fino a EVP_EncodeFinal. Tieni conto di 65 byte di output per blocco più il NUL, e non leggere *outl come "byte del mio payload" - sono i byte scritti da questa chiamata, che per una prima chiamata piccola è zero.
  • I pad mancanti dell'iteratore. La catena di Boost.Serialization non emette mai =. Un iteratore del 2002 che codifica "Mane" ti dà sei caratteri. Aggiungi i pad tu, oppure il tuo consumatore rigoroso rifiuterà la stringa.
  • Il CRLF che non hai chiesto. CryptBinaryToStringA aggiunge una coppia CR/LF a meno che non passi CRYPT_STRING_NOCRLF. Un token base64 costruito con le flag predefinite è due caratteri più lungo di quanto dovrebbe, e il penultimo carattere è un ritorno-a-carriage.
  • La chiamata NULL conta il NUL. La sonda di dimensione di Windows restituisce la lunghezza richiesta incluso il terminatore null; la chiamata vera restituisce la lunghezza senza. Confondere le due è il classico off-by-one, e scrive un byte oltre il buffer o perde l'ultimo carattere.
  • Avvolgi dopo, non durante. L'avvolgimento delle righe è un passo di post-elaborazione sulla stringa codificata. Taglia ai multipli della larghezza - sempre sicuro, perché ogni gruppo di 4 caratteri è autonomo - e non avvolgere mai i byte grezzi, che è il posto dove gli a capo non stanno.
  • Frammenti di tre. Se codifichi un grande payload a pezzi, i confini dei pezzi devono cadere su gruppi di 3 byte, altrimenti il raggruppamento - e l'output - cambia. 3072 è un frammento amichevole; 3071 è un bug.
  • int, non size_t. Ogni parametro di lunghezza EVP è un int. Il soffitto della singola chiamata è circa 2 GB di input, e l'output per quell'input non sta in un int nemmeno per sbaglio. Sopra il soffitto, il percorso a frammenti o in streaming non è una preferenza.
  • Char firmato. Se impacchetti da un char * senza il cast unsigned, un byte sopra 127 è un numero negativo su piattaforme dove char è firmato, e indicizzare una tabella con esso è comportamento indefinito. const unsigned char * non è una formalità.
  • La stringa JSON avvolta. Un valore avvolto a 76 caratteri incollato in un file JSON è una stringa di caratteri di controllo letterali. O è una riga, o è escapato, o non è in JSON.
  • I pad sono un contratto. Alcuni consumatori vogliono il padding (MIME, la maggior parte dei decoder), altri no (JWT, PKCE, token in URL), e alcuni rigorosi rifiutano i pad mancanti o non canonici senza appello. Il pad non è decorazione; fa parte dell'accordo del formato.
  • I due alfabeti. Un - o _ dentro un payload con alfabeto standard è non valido, e un + o / dentro uno URL-safe è non valido. Gli alfabeti non sono scambiabili a livello di byte - codifica con quello giusto per la destinazione, e trascodifica con deliberazione.
  • std::string e strlen. std::string porta volentieri byte a zero, ma nel momento in cui consegni una stringa C a un'API legacy, strlen si ferma al primo NUL. Passa puntatore e lunghezza, mai un puntatore nudo.
  • Il budget. L'output è 4/3 dell'input: se l'input è 1,5 GB, l'output è 2 GB - che è anche il soffitto di int. Dimensiona il buffer ricevente, la colonna e la rete con la formula, non a sentimento.

Come il C++ si è preso il suo Base64

La storia del formato è più vecchia dell'era moderna del linguaggio, e la storia del C++ è la storia del linguaggio che ripetutamente non lo ha spedito. Il primo uso standardizzato della codifica oggi chiamata base64 MIME è stato il protocollo Privacy-Enhanced Mail, proposto nel 1987 con righe da 64 caratteri e un controllo di integrità del messaggio RSA-MD2/MD5 incollato alla fine; il nome "base64" in sé è arrivato solo nel 1993, quando gli standard MIME gliel'hanno dato. Il C++ è arrivato come C++98 nel 1998 - cinque anni dopo il MIME - e il primo codice base64 che gli sviluppatori del linguaggio hanno preso in mano è stata la coppia C del 2004-2008 di Rene Nyffenegger, che una domanda su Stack Overflow del 4 dicembre 2008 ha diffuso per tutto il web. La parte più bella di quella storia: una risposta ha linkato la pagina stessa di Nyffenegger e ha portato l'implementazione qua, header di licenza incluso, e una risposta con i voti più alti ha misurato in benchmark la sua soluzione contro il resto del campo. La canzone popolare ha un header di licenza - il compositore in persona non è mai comparso nei commenti.

Poi l'ecosistema ha fatto ciò che gli ecosistemi fanno. Nel 2002, il Boost.Serialization di Robert Ramey ha spedito gli adattatori di iteratori - il base64 più vecchio della cassetta degli attrezzi C++, rigoroso nella direzione di decodifica e famigeratamente senza padding in quella di codifica, un anno prima che RFC 3548 codificasse le regole dell'alfabeto che già applicava. Nel 2017, Boost 1.66 ha portato Beast, e con esso il codec solo header che spedisce ancora oggi con l'attribuzione a Nyffenegger nel piè di pagina. L'EVP_EncodeBlock di OpenSSL e i suoi compagni sono in ogni release OpenSSL, quindi il cavallo da lavoro è nella cassetta degli attrezzi da quanto il linguaggio discute su se debba stare nello standard. Su Windows, la storia è semplicemente che il sistema operativo lo ha spedito: una funzione, una tabella di flag, nessun standard coinvolto. Intanto lo standard stesso è andato C++11, C++14, C++17, C++20, C++23 (pubblicato nel 2024) e ora C++26, e ognuno di essi, senza eccezioni, ha guardato l'alfabeto a 64 caratteri e ha proseguito. Il contenuto tecnico del C++26 è finito ed è stato votato e approvato (114-12-3) al meeting ISO C++ di marzo 2026 a Croydon, Regno Unito, e aggiunge davvero un nuovo header <text_encoding> per il lavoro sui codec di testo; i meeting successivi del comitato, di giugno 2026 (Brno) e novembre 2026 (Búzios, Brasile), aprono la bozza di lavoro del C++29 invece di rivedere il C++26. Il Base64 non è nello standard. Otto standard, tre decenni, un header per la codifica di testo - e il comitato ha ormai avuto ogni possibile scusa per aggiungere il base64 e ha rinunciato a tutte. La storia pratica del base64 in C++ è, e resta, la storia delle sue librerie: una coppia EVP, due varianti Boost, una flag di Windows, e uno snippet di quaranta righe che è tuo.

Cose sparse da sapere

  • Il blocco di 48 byte dell'encoder in streaming di OpenSSL è un numero che non compare in nessun RFC. Sono 16 gruppi base64, scelti in modo che la riga di output faccia esattamente 64 caratteri - l'abitudine PEM - ed è uno degli ultimi posti dove il 1987 sta ancora facendo lavoro portante nel 2026.
  • L'encoded_size di Boost.Beast è la sezione matematica come funzione constexpr: 4 * ((n + 2) / 3), valutata a tempo di compilazione quando gli dai una costante. La libreria standard non ha mai avuto questa riga sola; Boost l'ha spedita in uno spazio dei nomi detail:: invece.
  • Il base64 con padding più piccolo fa quattro caratteri, QQ==: un byte vestito da due caratteri. Quello senza padding più piccolo fa due caratteri, QQ. Il numero di pad è anche un messaggio: due pad significa che l'ultimo gruppo aveva un byte, un pad significa che ne aveva due, e nessun pad significa che ne aveva tre - il ricevente può recuperare la lunghezza dell'input dalla coda da sola.
  • La matematica dell'overhead di MIME è esatta: 4/3 per 78/76, ed è per questo che un allegato email arriva a circa il 137 percento della sua dimensione originale, più circa 814 byte di header. Ogni encoder di questo articolo paga la stessa tassa; la larghezza dell'avvolgimento cambia solo come viene fatturata.
  • Su un libstdc++ o MSVC tipico, std::string porta i payload piccoli in un buffer di stack tramite l'ottimizzazione delle stringhe corte invece di allocare. Un input di 9 byte si codifica in 12 caratteri e non tocca mai l'heap. La forma base64 del tuo token può letteralmente vivere in uno stack frame, ed è il genere di pranzo gratis che la libreria standard non pubblicizza.
  • Il comando openssl base64 a cui potresti arrivare in una shell non è affatto un comando. È il programma enc che controlla il proprio nome in argv[0] e cambia personalità. Un alias per confronto di stringhe, che è il modo C++ di fare le cose, in C.
  • Gli identificatori video di YouTube sono base64url: undici caratteri, senza padding, nessun + o / da nessuna parte vicino a un URL. Il formato di codifica più guardato al pianeta gira sulla variante "sicura per URL e nomi di file" che RFC 4648 ha aggiunto in una sezione che sta in una pagina.
  • Quattro A - AAAA - codificano tre byte a zero, perché A è lo zero dell'alfabeto. Se hai mai visto un blob base64 fatto interamente di un solo carattere, ora sai cosa stava dicendo: niente.
  • La stessa coppia di funzioni compare nelle risposte a una domanda su Stack Overflow del 2008, nel sorgente di Boost.Beast con un piè di pagina di attribuzione, e nei file di intestazione di innumerevoli basi di codice private. Chiedi a uno sviluppatore C++ da dove viene il suo base64 e la risposta più onesta è "non lo so, e nemmeno l'internet".

L'altra direzione

Tutto ciò che hai appena impacchettato verrà disimballato dalla stessa cassetta degli attrezzi dall'altra parte, e il lato disimballaggio ha il suo insieme di abitudini: la funzione a colpo singolo di OpenSSL che riempie di zeri la coda, la correzione di bug del 2025 che ha cambiato cosa restituisce il decoder in streaming per un input con padding, la decode di Boost.Beast che si ferma a un carattere fuori posto e non ne dice una parola, l'iteratore che fa eccezione su un singolo spazio, e il decoder rigoroso di quaranta righe che indica il byte esatto che ha fatto male. La storia completa del disimballaggio - i temperamenti dei quattro decoder, la trascodifica base64url, i file, l'abitudine dei 76 caratteri di MIME, e i due strumenti della riga di comando che falliscono in silenzio - vive nella guida C++ alla decodifica sul sito gemello. Vai a leggerla, poi torna e impacchetta qualcosa di grosso. Questa è l'intera partita: nessuna libreria standard, quattro fornitori con quattro opinioni diverse su a capo e NUL, una formula che dimensiona ogni buffer dell'articolo, e una tassa del 33 percento che ogni ricevente può rimborsare. Buon imballaggio.

Ultimo aggiornamento: 2026-09-08

Articolo correlato: Decodifica Base64 in C++ (Cpp): una guida completa