Vous avez à traiter le format Base64 ? Alors ce site est parfait pour vous ! Utilisez notre outil en ligne super pratique pour encoder ou décoder vos données.

Encodage Base64 en C++ (Cpp) : un guide complet

Le problème inverse est celui avec la plus grosse nouvelle : vous avez des octets - un certificat, une image, un blob aléatoire, une signature - et vous avez besoin qu'ils voyagent à travers quelque chose qui ne parle que texte : un champ JSON, un en-tête de courriel, une URL, une variable d'environnement. La page d'accueil de ce site passe le format en détail, alors voici seulement la version courte : trois octets deviennent quatre caractères de l'alphabet, une queue courte reçoit une ou deux marques =, et la forme encodée fait environ 33 pour cent de plus que l'original. L'encodage est la direction qui grandit, si bien que chaque buffer de cet article est dimensionné pour cela, et l'arithmétique tient en une ligne - 4 * ((n + 2) / 3) - qui ne change pas quel que soit l'encodeur que vous choisissez.

Comme le côté décodage, C++ lui-même n'encodera pas le moindre octet pour vous. La bibliothèque standard a eu trente ans pour faire pousser une fonction base64 et elle a passé tout ce temps sur d'autres choses, alors chaque programme C++ apporte son propre encodeur, choisi parmi quatre personnalités très différentes, plus l'option d'en écrire une quarantaine de lignes vous-même. L'un est une bête de somme qui porte le TLS depuis les années 1990 et qui fait du padding, termine par un nul, et coupe les lignes sans demander la permission. L'autre est un codec rapide header-only qui se cache dans un espace de noms que ses auteurs ont étiqueté « detail ». Un autre est un itérateur de 2002 qui n'a apparemment jamais croisé un caractère de padding. Un dernier est une fonction que le système d'exploitation livre depuis des décennies et qui ajoute CRLF à la fin de votre jeton. Et la cinquième option est la vôtre. Quand vous savez ce que chacun ajoute, refuse, ou ajoute discrètement, l'encodage cesse d'être une source de bugs off-by-one. Passons à l'emballage.

La norme n'a jamais livré d'emballeur

Toutes les normes depuis C++98 - et il y en a eu huit, jusqu'à C++26 - ont regardé l'alphabet de 64 caractères et sont passées à autre chose. Il n'y a pas de <base64>, pas de std::base64, rien dans <string> ou <vector> qui va emballer vos octets. Le travail technique de C++26 s'est terminé et a été voté (114-12-3) à la réunion ISO C++ de mars 2026 à Croydon, Royaume-Uni, et il ajoute bien un en-tête <text_encoding> pour le travail de codec de texte ; les réunions suivantes du comité, en juin 2026 (Brno) et en novembre 2026 (Búzios, Brésil), ouvrent le projet de travail C++29 au lieu de revenir sur C++26. Le base64 n'est pas dans la norme, et il est difficile de blâmer le comité : l'encodage de texte concerne les jeux de caractères, et le base64 concerne les octets, si bien que le nouvel en-tête n'a jamais été la bonne maison. En pratique, c'est l'écosystème qui a fait le travail. Les routines base64 EVP d'OpenSSL sont dans chaque version d'OpenSSL, les bibliothèques Boost portent deux encodeurs indépendants, Windows livre une fonction CryptoAPI avec une table de drapeaux pour le travail, et un extrait de quarante lignes est copié-collé à travers la langue depuis 2008. Si votre projet est basé sur CMake, tout le setup des dépendances tient en trois lignes :

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

La version de Boost à noter est la 1.92.0, d'août 2026, d'un projet fondé en 1998 qui livre des bibliothèques depuis sa première version en 1999. Les deux encodeurs Boost ci-dessous sont header-only - il n'y a rien à lier du tout - tandis qu'OpenSSL veut -lcrypto, que la plupart des programmes C++ qui touchent au TLS ont déjà dans le binaire.

D'abord, la math : dimensionner chaque buffer de cet article

Le Base64 groupe les octets par trois, si bien que la longueur de sortie a une forme qui ne surprend plus une fois qu'on la connaît : pour chaque 3 octets d'entrée, 4 caractères en sortie, et une queue courte reçoit du padding jusqu'à un groupe complet. Le décompte exact pour n octets d'entrée est :

4 * ((n + 2) / 3)

Le +2, c'est l'astuce du plafond : la division entière arrondit vers le bas, si bien que l'ajout de 2 avant fait arrondir vers le haut au multiple de trois suivant. À partir de là, chaque taille de buffer de cet article est une substitution. La fonction one-shot d'OpenSSL veut un buffer capable de contenir les données encodées plus le NUL qu'elle ajoute à la fin - la page de manuel illustre le contrat avec 16 octets d'entrée qui deviennent 24 octets encodés plus 1 NUL, 25 octets au total, et la fonction renvoie la longueur sans le NUL. Son chemin en streaming traite l'entrée par blocs de 48 octets, et la page de manuel dimensionne la sortie à 65 octets par bloc (64 caractères plus le retour à la ligne que chaque bloc produit toujours) plus un octet de plus pour le NUL. L'en-tête Boost.Beast vous remet la formule exacte sous forme de fonction constexpr. Et votre propre code réserve (n + 2) / 3 * 4 et s'en tient là. Voici les chiffres que vous rencontrerez vraiment :

Entrée Sortie (avec padding) Ce qu'il faut remarquer
1 octet 4 caractères La forme avec padding la plus petite : QQ==
2 octets 4 caractères Trois caractères de données et un pad
3 octets 4 caractères Un groupe complet, pas de padding du tout
48 octets 64 caractères Exactement un bloc en streaming d'OpenSSL
500 octets 668 caractères Enveloppé à 64, ce sont 11 lignes, 679 caractères avec les retours à la ligne
1 Go environ 1,33 Go Dimensionnez la colonne, le fichier, et le réseau pour l'impôt

Si le côté récepteur est une colonne de taille fixe, un buffer, ou une ligne dans un fichier texte, cette formule est tout le document de conception. La seule direction dans laquelle elle peut vous mordre est l'autre : le côté décodage a besoin de 3n/4 moins les pads, et un buffer de décodage dimensionné avec la formule d'encodage est une sur-allocation classique qui grandit jusqu'à devenir un ticket de bug mémoire. Dimensionner la direction qui rétrécit est le problème du guide soeur ; ici, vous ne faites que grandir.

Voici la situation, parce que les différences sont toutes dans les extras - le padding, les retours à la ligne, les NUL - plutôt que dans l'emballage de base, que chaque ligne implémente identiquement :

Encodeur D'où il vient Padding Octets en plus à budgéter Particularité à retenir
EVP_EncodeBlock <openssl/evp.h>, lien -lcrypto Toujours 1 (un NUL dans le buffer) L'exemple des 16 octets de la page de manuel est le contrat
EVP_EncodeUpdate + Final idem Toujours 65 par bloc de 48 octets Enveloppe fermement à 64 caractères, chaque bloc finit par un retour à la ligne
Boost.Beast encode boost/beast/core/detail/base64.hpp, header-only Toujours 0 Se trouve dans un espace de noms nommé detail
Les itérateurs Boost.Serialization boost/archive/iterators/base64_from_binary.hpp, header-only Jamais 0 - vous ajoutez les 1 ou 2 pads vous-même Le plus ancien encodeur de la boîte à outils, 2002
CryptBinaryToStringA wincrypt.h, crypt32.lib Toujours 2 (un CRLF) sauf NOCRLF A un drapeau URL-safe que le reste de la boîte à outils n'a pas
Vos quarante lignes à vous Nulle part : c'est à vous Votre choix Votre choix Vous possédez chaque cas limite pour toujours

L'algorithme de base est identique dans chaque ligne - c'est la partie rassurante d'un format de 1987. Ce qui diffère est ce que chaque implémentation ajoute autour du payload, et presque chaque piège de cet article est l'une de ces additions qui rencontre un consommateur qui ne l'attendait pas.

OpenSSL : l'encodeur que votre pile TLS lie déjà

Si votre programme lie déjà OpenSSL pour le TLS, vous n'avez rien à ajouter. La fonction one-shot tient en un seul appel :

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

Donnez-lui les octets source et la longueur, et elle écrit l'encodage avec padding, sur une ligne. Le contrat mérite d'être mémorisé, parce que la page de manuel l'énonce avec un exemple : pour chaque 3 octets d'entrée, 4 octets de sortie ; une queue qui n'est pas divisible par 3 reçoit du padding pour que la sortie soit toujours divisible par 4 ; et un caractère de terminaison NUL est ajouté en plus. L'exemple documenté fait 16 octets en entrée, 24 octets encodés plus 1 NUL, 25 octets au total dans le buffer, la fonction renvoyant 24 - la longueur sans le NUL. Dimensionnez le buffer en conséquence et le wrapper tient en quelques lignes :

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

Remarquez ce que le std::string fait et que le C vous imposerait : il grandit à exactement la longueur renvoyée, si bien que le NUL ajouté par OpenSSL est simplement au-delà de la longueur suivie et ne fait jamais partie du payload. Encodiez « Mane » et vous obtenez TWFuZQ==, la queue classique de quatre caractères avec son pad unique ; encodez un octet et vous obtenez une paire de deux caractères de données déguisée en deux caractères de padding ; et si vous n'encodez rien, vous obtenez la chaîne vide, le seul cas où un encodeur base64 se comporte exactement comme la fonction identité. La seule ligne de vraie logique dans toute la fonction est le resize : elle transforme « octets écrits plus un NUL » en « exactement le payload ».

Pour les données qui arrivent par morceaux - un fichier, un socket, un flux que vous ne voulez pas mettre en buffer - OpenSSL a un contexte que vous alimentez et terminez, et l'arithmétique de blocs de la page de manuel est inhabituellement explicite. Seuls les blocs complets de 48 octets sont traités immédiatement ; tout reste est retenu dans le contexte et libéré par un appel ultérieur ou par le dernier. Chaque bloc traité écrit 64 caractères plus un retour à la ligne - 65 octets - et l'appel final gère le bloc partiel, c'est pourquoi son plafond documenté est de 65 octets plus le NUL. La conséquence à connaître avant d'appeler : cette API enveloppe à 64 caractères. Elle n'est pas configurable. C'est ça, l'encodeur en 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());
}

Alimentez-le avec 500 octets de la lettre A et la comptabilité sort exactement comme la page de manuel l'avait promis : 668 caractères encodés, et parce que la sortie est coupée en lignes de 64 caractères, vous obtenez 11 lignes, 679 caractères au total, et tout le dernier caractère est un retour à la ligne. C'est ce retour à la ligne final qui casse les consommateurs : collez le résultat dans une chaîne JSON et vous avez un caractère de contrôle là où une guillemet devait être ; utilisez-le comme segment de jeton et vous avez inventé un nouveau segment. La règle de base : l'API par blocs pour les payloads d'une ligne (jetons, en-têtes, valeurs de configuration), l'API en streaming quand le consommateur veut une sortie enveloppée à la forme MIME, et quand vous doutez, retirez le retour à la ligne final avec une boucle while (out.back() == '\n') avant que le payload ne franchisse une frontière qui ne l'attend pas.

Boost.Beast : un emballeur rapide dans un espace de noms detail::

La bibliothèque HTTP de Boost livre un codec base64 à l'adresse improbable boost/beast/core/detail/base64.hpp. L'espace de noms detail:: est la façon qu'a Boost de dire « c'est notre affaire interne », et les mainteneurs ont refusé de promouvoir le codec en API publique. Tout le monde l'utilise quand même : il est petit, il est rapide, il est header-only (définissez BOOST_BEAST_HEADER_ONLY avant l'include et il n'y a rien à lier), et c'est le même codec que le propre handshake WebSocket de Boost.Beast utilise pour le calcul de Sec-WebSocket-Accept, ce qui veut dire qu'il mâche du trafic réel depuis des années.

Sur le côté encodage, l'API est d'un calme presque insultant. Un outil constexpr vous donne la taille exacte de la sortie - 4 * ((n + 2) / 3), la même formule que la section de math, désormais avec un compilateur pour la vérifier - et la fonction encode écrit le résultat avec padding dans votre buffer et vous dit combien de caractères elle a utilisés. Il n'y a pas de canal d'erreur, parce que l'encodage ne peut pas échouer : n'importe quel octet est une entrée valide, et la longueur de sortie est une fonction pure de la longueur d'entrée. Le 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());
}

Encodiez « Mane » et vous obtenez TWFuZQ== ; encodez l'octet unique M et vous obtenez TQ== - les mêmes octets que le wrapper OpenSSL produisait, sans NUL à craindre et sans lignes à retirer. Deux choses à ranger. D'abord, la provenance : le source est sous copyright 2016-2019 de Vinnie Falco, avec un pied de page qui attribue des portions à un extrait de Rene Nyffenegger de 2004-2008 - le même chant populaire qui a commencé l'histoire du base64 en C++, désormais livré dans Boost, dans votre binaire, en train de faire des handshakes WebSocket pour tout le web. Ensuite, la pratique : parce que le codec fait du padding et n'enveloppe jamais, c'est l'outil juste pour tout ce qui doit tenir sur une ligne - jetons, en-têtes, payloads d'API - et la formule encoded_size vous donne un buffer parfaitement exact, jamais une approximation.

Boost.Serialization : l'itérateur qui a oublié que le padding existe

Le base64 le plus ancien de l'écosystème C++ n'est pas une fonction mais une série d'adapteurs d'itérateurs composables, écrits par Robert Ramey en 2002 pour la bibliothèque de sérialisation de Boost. La direction d'encodage est une chaîne de deux adapteurs : un transformateur de largeur qui regroupe vos octets bruts huit vers six, et un itérateur qui transforme chaque valeur regroupée en caractère de l'alphabet :

#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'itérateur fait l'emballage de base et rien d'autre - pas de padding, pas de NUL, pas de retours à la ligne, et pas de canal d'erreur, parce que l'emballage de base ne peut pas échouer. Encodez « Mane » et l'itérateur vous remet six caractères, TWFuZQ, sans changer d'expression : un vrai encodage de quatre octets fait huit caractères, et il ne serait jamais venu à l'idée d'un itérateur de 2002 de s'en soucier. C'est pourquoi la déclaration switch porte la charge, elle n'est pas décorative : un groupe manquant d'un octet reçoit deux pads, manquant de deux octets en reçoit un. La même chaîne sans le switch, c'est ce que vous obtenez si vous oubliez cette étape, et le résultat est une chaîne qui se décode bien sous un décodeur indulgent, échoue sous un décodeur strict, et transforme le message d'erreur de votre consommateur d'API en mystère. (Le côté décodage de cette même famille d'itérateurs est celui qui lève l'exception au premier espace égaré - plus de détails dans le guide soeur.)

Quarante lignes, zéro dépendance

Le Base64 est assez petit pour qu'un encodeur correct mérite d'être possédé, et en C++ le gain est meilleur que dans n'importe quelle autre langue : std::string rend la gestion des buffers agréable, la formule vous donne la taille exacte d'emblée, et un encodeur fait main est celui qui n'a aucun avis du tout - pas de NUL, pas de retours à la ligne, pas d'habitude de plateforme - ce qui est exactement ce que vous voulez sous un fichier de configuration ou une frontière d'API. Cette version embarque par groupes de 3 octets contre une table de 64 caractères :

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

Parcourons les morceaux. La ligne reserve est la section de math : (n + 2) / 3 * 4 caractères, exactement, si bien qu'il n'y a pas de réallocation en plein milieu de la boucle. Le reinterpret_cast en const unsigned char * n'est pas un rituel - sur une plateforme où char est signé, un octet au-dessus de 127 serait sinon un nombre négatif, et au moment où il toucherait un indice de table vous auriez un comportement indéfini en blouse blanche. Chaque itération tire jusqu'à trois octets dans une valeur de 24 bits, pousse les quatre tranches de 6 bits dans la table, et dans la queue émet = à la place de l'octet qui n'y était pas - les gardes i + 1 < n et i + 2 < n sont toute la logique de padding. Donnez-lui « Mane » et vous obtenez TWFuZQ==. Donnez-lui un M unique et vous obtenez TQ==. Donnez-lui des octets au-dessus de 127 - la paire 0xCA 0xBF sur la troisième ligne de l'exemple - et la sortie reste du pur ASCII (Tcq/), parce qu'un octet au-dessus de 127 est juste un octet, et la table ne se soucie pas de ce qu'il signifie. Quarante lignes, zéro dépendance, et chaque cas limite est une ligne que vous avez écrite, ce qui est tout l'enjeu.

Windows CryptoAPI : l'emballeur intégré au système

Sous Windows, il y a un encodeur base64 dans le système d'exploitation lui-même, plus vieux que la plupart des frameworks de cet article : CryptBinaryToStringA de wincrypt.h, dans crypt32.lib, une partie de la CryptoAPI qui est livrée avec Windows depuis des décennies. Il convertit un tableau d'octets en chaîne formatée, et sa table de drapeaux se lit comme un menu de toute l'histoire du format :

Drapeau Valeur Ce que vous obtenez
CRYPT_STRING_BASE64HEADER 0x0 Base64 enveloppé dans les lignes d'en-tête BEGIN/END de certificat
CRYPT_STRING_BASE64 0x1 Base64 tout simple, sans en-têtes
CRYPT_STRING_BASE64URI 0xD L'alphabet URL-safe : + devient -, / devient _, selon la section 5 de la RFC 4648
CRYPT_STRING_NOCRLF 0x40000000 Aucun retour à la ligne ajouté à la fin
CRYPT_STRING_NOCR 0x80000000 Un simple LF au lieu du CRLF par défaut

La première chose à savoir est le défaut : à moins de passer CRYPT_STRING_NOCRLF, la fonction ajoute une paire retour chariot/retour à la ligne à la fin de votre chaîne - le comportement documenté est que chaque format non binaire reçoit une séquence de saut de ligne - si bien qu'un jeton base64 qui doit tenir sur une ligne veut BASE64 | NOCRLF, et cette combinaison est l'appel idiomatique. La seconde chose est la convention d'appel, qui est le pas de deux classique Windows : appeler avec un buffer NULL pour demander combien d'espace est nécessaire (la réponse inclut le NUL de terminaison), allouer, appeler à nouveau, et lire la longueur sans le 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;
}

Deux notes de plus. Le drapeau URI est le seul base64url natif de tout cet article - sous Windows vous pouvez encoder l'alphabet des jetons directement, et l'approche de transcodage de la section ci-dessous est strictement pour les autres plateformes. Et l'entrée CRYPT_STRING_BASE64HEADER, avec sa valeur 0, est aussi le drapeau que vous obtenez si vous passez zéro, si bien qu'un appel qui « voulait dire » aucun drapeau du tout enveloppe discrètement le payload dans les lignes d'en-tête de certificat - l'habitude de cadrage de l'ère PEM, utile pour générer des fichiers .pem et une surprise pour tout le reste. Liez contre crypt32.lib et la fonction est à vous pour le reste de la vie du programme.

Base64url : l'alphabet des jetons et des URL

L'alphabet standard a deux caractères qui ne survivent pas à une URL : + signifie espace dans une chaîne de requête, et / signifie répertoire dans un chemin. La section 5 de la RFC 4648 corrige cela avec deux échanges de caractères - + devient - et / devient _ - et est nette sur le résultat : cet encodage « ne devrait pas être considéré comme identique à l'encodage base64 ». C'est l'alphabet des JWT, des défis de code OAuth PKCE, des identifiants de vidéo YouTube, et de la plupart des jetons d'API, et il supprime couramment le padding = aussi, parce que dans un jeton la longueur est connue implicitement et les pads ne seraient que des échappements en pourcentage en attente de se produire.

Parmi les encodeurs de cet article, seul le drapeau Windows émet l'alphabet nativement - OpenSSL n'a pas de mode URL-safe, et aucune saveur Boost non plus - si bien que sur la plupart des plateformes la recette est : encoder en standard, échanger les deux caractères, retirer les pads. Cela tient en une douzaine de lignes :

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

/* base64_encode depuis la section "Quarante lignes, zéro dépendance" */

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 première ligne de sortie est Tcq_, là où l'alphabet standard aurait écrit / ; les deux lignes suivantes montrent le commutateur de padding en action - TQ sans padding par défaut, TQ== quand le consommateur le veut de retour. Cet argument pad est celui sur lequel réfléchir, parce que les consommateurs ne sont pas d'accord : les segments de JWT ne veulent pas de pads, les défis PKCE ne veulent pas de pads, mais une valeur base64url qui finit dans un champ où le décodeur est strict sur la longueur peut les vouloir de retour, et le commutateur est un bool, pas une réécriture. Et le mode d'échec à retenir dans la direction opposée : un - dans un payload à alphabet standard est simplement invalide, si bien que les deux alphabets ne sont pas interchangeables au niveau des octets - un jeton encodé avec le mauvais alphabet ne se décode pas, il échoue, ce qui est l'échec que vous voulez à une frontière de sécurité.

Coupure de lignes : 64, 76, ou jamais

Le base64 enveloppé a trois longueurs de ligne dans la nature, chacune avec son histoire. L'encodeur en streaming d'OpenSSL est calé à 64 caractères - l'habitude PEM, où la norme de 1987 de Privacy-Enhanced Mail enveloppait à 64. Le MIME, en standardisant l'encodage pour le courriel en 1993, est passé à 76 caractères, et ce chiffre est le défaut de la commande base64 de coreutils (son drapeau -w fixe la largeur, et -w 0 éteint l'enveloppement complètement) et de la plupart des outils de l'écosystème. La RFC 4648 elle-même ne prend parti pour personne : elle cite 76 comme la limite du MIME et dit aux implémentations de ne pas envelopper du tout sauf si la spécification qui s'y réfère le leur indique. Celui que vous émettez dépend de qui le consomme, et le consommateur - pas le format - est la contrainte de conception.

L'enveloppement est une étape de post-traitement sur la chaîne encodée, jamais une étape d'entrée : les groupes de 4 caractères sont l'unité de sens, si bien que couper la chaîne à n'importe quel multiple de la largeur est une coupe sûre - chaque frontière de ligne tombe entre deux groupes. La version C++ est une boucle :

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

/* base64_encode depuis la section "Quarante lignes, zéro dépendance" */

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());
}

La comptabilité : 200 octets s'encodent en 268 caractères, et enveloppé à 76 avec des terminaisons CRLF ce sont 4 lignes - trois lignes pleines et une queue de 40 caractères - 276 caractères sur le câble. Le choix CRLF de l'extrait est le choix du courriel ; pour tout le reste, le LF est le défaut moderne, et la seule règle qui n'est pas négociable est la cohérence - un décodeur qui s'attend à du CRLF lira un seul LF comme un caractère de données s'il est strict. (La règle du MIME est que les décodeurs doivent ignorer les sauts de ligne, c'est pourquoi le courriel n'a jamais souffert de la différence.) La troisième habitude à connaître : la commande openssl base64 - qui est le programme enc en trencot, qui vérifie son propre nom dans argv[0] - enveloppe à 64 sans -A et émet une ligne avec -A, et c'est l'unique outil de la ligne de commande dont on vérifie le comportement à chaque exécution au lieu de lui faire confiance depuis la mémoire.

Binaire dans JSON et configuration

Une chaîne JSON a une petite liste de caractères qu'elle ne peut pas contenir bruts : le guillemet, l'antislash, et les caractères de contrôle en dessous de 0x20. Un certificat, une clé aléatoire, une signature - tous en sont pleins - des octets qui deviendraient une cascade d'échappements s'ils essayaient de voyager dans un champ de chaîne brute, et les caractères de contrôle feraient carrément s'étouffer certains analyseurs. Le base64 est la solution, et c'est la réponse par défaut que donne tout format de configuration qui doit porter du binaire : la valeur est stockée comme une seule ligne de caractères de pur alphabet, et les règles de citation de la bibliothèque JSON n'ont plus rien à faire.

Le schéma C++ est toute l'implémentation : lire les octets (en mode binaire, évidemment), encoder, stocker la chaîne. Le consommateur décode de l'autre côté. Le seul piège propre à JSON est la chaîne enveloppée : un certificat enveloppé à 76 caractères collé directement dans un fichier JSON est une chaîne pleine de caractères de contrôle littéraux, ce qui est soit une erreur d'analyse soit une corruption silencieuse, selon l'humeur de l'analyseur. Si la valeur doit être enveloppée pour les yeux humains, elle doit être échappée ou tenir sur une ligne - et pour la configuration machine à machine, une ligne est la réponse. L'autre piège est la valeur sans étiquette : une colonne de configuration qui dit base64 dans une page de manuel de 2014 est généralement de l'alphabet standard avec padding, mais les jetons de l'ère API sont du URL-safe sans padding, et le test à quatre caractères du guide de décodage - contient-il + ou /, - ou _, un = à la fin ? - est tout le diagnostic.

Les data URIs : des fichiers qui se collent dans des pages

Une data URI est une URL dont le payload est là, dans l'adresse : data:, un type de média facultatif, un marqueur ;base64 facultatif, une virgule, et les données elles-mêmes - tout le schéma de la RFC 2397. Les navigateurs s'en servent pour embarquer des images, des polices et de petits scripts directement dans le HTML et le CSS sans requête supplémentaire, et si une page continue de fonctionner avec le réseau désactivé, une data URI est une forte suspecte. Côté C++, le travail d'encodage est d'assembler la chaîne, ce qui est de la concaténation de chaînes avec une constante :

#include <cstdio>
#include <string>

/* base64_encode depuis la section "Quarante lignes, zéro dépendance" */

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());
}

Les pièges sont tous dans les détails. Le marqueur ;base64 fait exactement sept caractères, ce qui est la longueur que les bugs off-by-one choisissent d'attaquer : un analyseur qui en vérifie six est un analyseur qui accepte data:text/plain;base4,... et qui décode des ordures sans changer d'expression. Et le payload d'une data URI base64 tient sur une ligne - les retours à la ligne ne font pas partie de la grammaire des URI, si bien que si votre encodeur a enveloppé l'image à 76 (et les encodeurs à forme MIME le feront, par défaut), l'URI est cassée avant d'atteindre le navigateur. La règle pour ce consommateur : encoder, ne pas envelopper, et garder le type de média précis - un image/png faux sur un JPEG est le genre de mensonge qui ne se montre que comme une miniature cassée à 2 heures du matin.

Jetons : JWT, PKCE et clés API

Le base64 le plus risqué sur internet est dans un jeton. Un JSON Web Token est trois segments base64url collés avec des points : un JSON d'en-tête, un JSON de revendications, et une signature calculée sur la chaîne header.claims. C++ n'a pas de type JWT intégré, mais en construire un tient dans l'encodeur base64url ci-dessus plus un appel HMAC, parce que le jeton tout entier est du base64url jusqu'à ce que ce ne soit plus - jusqu'à ce que ce soit une signature :

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

/* base64_encode et base64url_encode depuis les sections précédentes */

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());
}

Exécutez l'exemple et le jeton qui sort est un jeton HS256 tout droit sorti du manuel : l'en-tête se décode en {"alg":"HS256","typ":"JWT"}, les revendications en un sujet, un nom, et un horodatage d'émission, et la signature est le base64url d'un HMAC-SHA256 sur les deux segments encodés. Trois détails portent tout le design. L'entrée de signature est les segments encodés, pas le JSON brut - signez le JSON et vous avez signé les mauvais octets. Les segments sont du base64url sans padding - les pads seraient au milieu d'une URL, et tout l'intérêt de l'alphabet était de garder le jeton comme une seule chaîne propre. Et HS256 veut dire un secret partagé, ce qui est un algorithme serveur à serveur : un secret qui vit dans le code d'un client n'est pas un secret, et le jeton qu'il signe n'est pas une accréditation. (Le flux PKCE d'OAuth utilise le même alphabet à un degré près : un vérificateur aléatoire, haché avec SHA-256, mis en base64url sans pads en un défi de code - l'encodeur de la section base64url est toute l'implémentation côté client.)

Authentification HTTP Basic

Le base64 le plus vieux dans HTTP est l'en-tête d'identifiants : Authorization: Basic suivi du base64 de user:password, un schéma si vieux qu'il précède JSON. Le construire tient dans une concaténation :

#include <cstdio>
#include <string>

/* base64_encode depuis la section "Quarante lignes, zéro dépendance" */

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());
}

La sortie est la chaîne que vous avez probablement vue dans une requête capturée : Basic dXNlcjpwYXNzd29yZA==. Deux notes C++. La concaténation user + ":" + pass est l'endroit où un mot de passe contenant un deux-points embrouillerait un analyseur naïf de l'autre côté - la règle d'analyse est « couper au premier deux-points », c'est pourquoi le côté construction est libre de mettre n'importe quoi dans les deux champs. Et si les identifiants ne sont pas en ASCII, la lecture sûre du schéma est de traiter l'identifiant utilisateur et le mot de passe en UTF-8 avant le base64, ce qui en C++ veut dire que votre std::string fait déjà le travail - à condition de l'avoir rempli d'octets UTF-8 et non de ce que la locale a décidé. La note de sécurité appartient à chaque mention de ce schéma : l'authentification Basic est de l'obfuscation, pas de la protection. L'en-tête voyage en clair pour quiconque peut lire le réseau, donc il n'est acceptable que derrière TLS, et même là, c'est le choix pour les appels machine à machine, pas pour des gens. (Le codec de Boost.Beast - celui de l'espace de noms detail:: - fait ce même travail d'en-tête dans l'implémentation WebSocket de Boost, en mettant en base64 le digest SHA-1 qui devient la clé Sec-WebSocket-Accept, ce qui est la preuve discrète que le schéma fait cela depuis 2017.)

Courriel : règles sur sept bits, réponse base64

Le courriel est l'endroit où le base64 a appris ses habitudes, et les habitudes sont encore porteuses. SMTP, dans sa forme originale, était conçu pour transporter du ASCII sur sept bits, si bien que tout ce qui était binaire devait être réécrit en texte imprimable avant de pouvoir voyager. Privacy-Enhanced Mail l'a fait en 1987 avec des lignes de 64 caractères et un contrôle d'intégrité de message RSA-MD2/MD5 collé à la fin, et le MIME, en standardisant l'encodage pour le courriel en 1993, a assoupli la limite à 76 caractères et ajouté la règle selon laquelle un décodeur conforme doit simplement ignorer les sauts de ligne. Une pièce jointe de courriel est toujours du base64 aujourd'hui, enveloppée à 76, et l'arithmétique exacte donne 4/3 fois 78/76 - environ 137 pour cent de la taille d'origine, plus environ 814 octets d'en-têtes.

Le côté C++, c'est l'encodeur plus la fonction d'enveloppement ci-dessus - encoder sur une ligne, envelopper à 76 avec CRLF, c'est fait. Les deux détails propres au courriel : la dernière ligne peut ou non porter un retour à la ligne final (les décodeurs sont obligés de l'ignorer, si bien que les deux sont légaux et les deux sont courants), et la valeur enveloppée n'est pas une valeur JSON, une variable d'environnement, ou un jeton - c'est un blob qui a sa place dans un corps MIME, et le déplacer ailleurs est l'endroit où l'enveloppement cesse d'être une habitude et commence à être un bug. La direction inverse - une pièce jointe qui arrive enveloppée à 76 - est le territoire du guide soeur, où les quatre décodeurs C++ ne sont pas d'accord sur les sauts de ligne, chacun à sa façon.

Fichiers, flux et le plafond de deux gigaoctets

Encoder un fichier est l'image miroir du travail sur les fichiers du guide de décodage : ouvrir en mode binaire (sous Windows, une lecture en mode texte traduirait les paires CRLF en retours à la ligne simples et modifierait vos données avant que l'encodeur ne les voie), lire les octets, encoder, écrire en binaire. La version petit fichier tient en une fonction :

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

/* base64_encode depuis la section "Quarante lignes, zéro dépendance" */

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());
}

Le plafond est le fait propre à C++ dans le titre de la section : chaque paramètre de longueur de l'API EVP est un int. Un seul appel EVP_EncodeBlock peut donc encoder au plus environ 2 Go d'entrée, et le buffer de sortie de cet appel - 1,33 fois plus gros - ne tient tout simplement pas dans un int. Sous le plafond, l'API par blocs va bien pour les fichiers qui tiennent en mémoire. Au-dessus, ou pour un fichier que vous ne voulez pas en mémoire, vous découpez en blocs - et la règle de découpage est la seule contrainte propre au base64 sur la boucle : les blocs doivent être des multiples de 3 octets, parce que le regroupement est par trois et une frontière de bloc au milieu d'un groupe change la sortie. 3072 - trois blocs de 1024 octets - est une taille de bloc confortable, et la boucle devient :

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

/* base64_encode depuis la section "Quarante lignes, zéro dépendance" */

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

Chaque bloc s'encode indépendamment et la concaténation est identique au résultat one-shot - ce qui est la propriété qui rend le découpage sûr, au fond, et elle tombe directement du regroupement par 3 octets. (Le contexte en streaming d'OpenSSL de la section encodeur fait le même travail en ajoutant l'enveloppement de lignes à 64 caractères gratuitement, ce qui est l'outil juste quand le consommateur veut la forme MIME.) Et le côté sortie a le même budget que le côté entrée : un fichier de 10 Go devient une chaîne de 13,3 Go, si bien que le buffer - ou le fichier que vous écrivez - est dimensionné avec la formule de la section de math, et le plafond en int dit que le chemin par blocs n'est pas une commodité au-dessus de 2 Go - c'est le seul chemin.

Variables d'environnement et ligne de commande

Les variables d'environnement ont le même problème que les chaînes JSON et une réponse pire : elles ne peuvent pas porter d'octets NUL du tout, et les caractères de contrôle ne sont pas leurs amis non plus. L'astuce standard est de mettre le payload en base64 pour qu'il survive au shell, et en C++ la direction d'encodage tient en une ligne :

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

/* base64_encode depuis la section "Quarante lignes, zéro dépendance" */

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

La valeur qui atterrit dans l'environnement est aGVsbG8sIGVudg== : alphabet pur, sûr pour le shell, sûr pour un fichier .env, sûr pour un tableau de bord CI, et décodable sur n'importe quelle machine qui a un décodeur base64. La ligne de commande elle-même a la même histoire à deux outils que le côté décodage, avec les drapeaux de la direction d'encodage : base64 de coreutils (ou la réimplémentation uutils que les distributions plus récentes livrent ; vérifiez avec base64 --version) enveloppe à 76 par défaut, et -w 0 vous donne une ligne ; openssl base64 - le programme enc qui vérifie son propre nom dans argv[0] et se bascule en mode base64 - enveloppe à 64 et prend -A pour une ligne unique :

# une ligne, pour les jetons et la configuration
base64 -w 0 < payload.bin > payload.b64
openssl base64 -A < payload.bin > payload.b64

# enveloppé, pour le courriel et les fichiers texte
base64 < payload.bin > payload-76.b64
openssl base64 < payload.bin > payload-64.b64

Aucun des deux ne parle base64url nativement, si bien qu'un jeton que vous frappez dans un shell reçoit le traitement de transcodage avant d'entrer dans une URL. Et la ligne de commande est l'endroit où l'habitude d'échec silencieux du côté encodage est la plus dangereuse : un encodeur qui a enveloppé quand votre consommateur s'attendait à une ligne ne signalera pas d'erreur, il produira juste une chaîne avec des retours à la ligne dedans - c'est exactement l'échec que vous chassez maintenant en production. Pour tout ce qui compte, encodez dans votre programme, là où le buffer est dimensionné par la formule et où la forme des lignes est une variable que vous contrôlez.

Les pièges : édition C++

  • Le NUL que vous n'avez pas commandé. EVP_EncodeBlock ajoute un NUL de terminaison après le payload. L'exemple de la page de manuel : 16 octets en entrée, 24 encodés plus le NUL, 25 dans le buffer, 24 renvoyés. Dimensionnez pour l'octet en plus et redimensionnez à la valeur renvoyée, sinon votre jeton finit par un octet nul.
  • Le 64 fixe. L'API en streaming d'OpenSSL enveloppe à 64 caractères, chaque bloc finit par un retour à la ligne, et il n'y a pas de drapeau pour le changer. Une sortie d'encodeur enveloppée dans un consommateur d'une ligne est un bug de caractère de contrôle.
  • Le bloc de 48 octets. EVP_EncodeUpdate n'émet de sortie que pour des blocs d'entrée complets de 48 octets ; le reste reste dans le contexte jusqu'à EVP_EncodeFinal. Budgétisez 65 octets de sortie par bloc plus le NUL, et ne lisez pas *outl comme « octets de mon payload » - ce sont les octets écrits par cet appel, ce qui, pour un premier appel petit, est zéro.
  • Les pads manquants de l'itérateur. La chaîne Boost.Serialization n'émet jamais =. Un itérateur de 2002 qui encode « Mane » vous donne six caractères. Ajoutez les pads vous-même, sinon votre consommateur strict refusera la chaîne.
  • Le CRLF que vous n'avez pas demandé. CryptBinaryToStringA ajoute une paire CR/LF à moins de passer CRYPT_STRING_NOCRLF. Un jeton base64 construit avec les drapeaux par défaut est deux caractères plus long qu'il ne devrait, et l'avant-dernier caractère est un retour chariot.
  • L'appel NULL compte le NUL. La sonde de taille Windows renvoie la longueur nécessaire y compris le nul de terminaison ; l'appel réel remet la longueur sans lui. Confondre les deux est l'off-by-one classique, et il écrit un octet au-delà du buffer ou perd le dernier caractère.
  • Envelopper après, pas pendant. L'enveloppement de lignes est une étape de post-traitement sur la chaîne encodée. Coupez à des multiples de la largeur - toujours sûr, parce que chaque groupe de 4 caractères est autonome - et n'enveloppez jamais les octets bruts, ce n'est pas là que les sauts de ligne ont leur place.
  • Des blocs de trois. Si vous encodez un gros payload par morceaux, les frontières de morceaux doivent tomber sur des groupes de 3 octets, sinon le regroupement - et la sortie - changent. 3072 est un bloc amical ; 3071 est un bug.
  • int, pas size_t. Chaque paramètre de longueur EVP est un int. Le plafond d'un appel unique est d'environ 2 Go d'entrée, et la sortie pour cette entrée ne tient tout simplement pas dans un int. Au-dessus du plafond, le chemin par blocs ou en streaming n'est pas une préférence.
  • Le char signé. Si vous emballez depuis un char * sans le cast unsigned, un octet au-dessus de 127 est un nombre négatif sur les plateformes où char est signé, et indexer une table avec lui est un comportement indéfini. const unsigned char * n'est pas un rituel.
  • La chaîne JSON enveloppée. Une valeur enveloppée à 76 caractères collée dans un fichier JSON est une chaîne de caractères de contrôle littéraux. Soit elle est sur une ligne, soit elle est échappée, soit elle n'est pas dans JSON.
  • Les pads sont un contrat. Certains consommateurs veulent du padding (MIME, la plupart des décodeurs), d'autres non (JWT, PKCE, jetons dans des URL), et quelques-uns stricts refusent carrément les pads manquants ou non canoniques. Le pad n'est pas une décoration ; il fait partie de l'accord de format.
  • Les deux alphabets. Un - ou _ dans un payload à alphabet standard est invalide, et un + ou / dans un payload URL-safe est invalide. Les alphabets ne sont pas interchangeables au niveau des octets - encodez avec le bon pour la destination, et transcodez délibérément.
  • std::string et strlen. std::string porte les octets nuls sans souci, mais dès que vous remettez une chaîne C à une API héritée, strlen s'arrête au premier NUL. Passez le pointeur et la longueur, jamais un pointeur nu.
  • Le budget. La sortie fait 4/3 de l'entrée : si l'entrée est de 1,5 Go, la sortie est de 2 Go - ce qui est aussi le plafond en int. Dimensionnez le buffer récepteur, la colonne, et le câble avec la formule, pas avec une estimation.

Comment C++ a obtenu son Base64

L'histoire du format est plus vieille que l'ère moderne de la langue, et l'histoire C++ est celle d'une langue qui ne le livre jamais, encore et encore. Le premier usage standardisé de l'encodage désormais appelé base64 MIME était le protocole Privacy-Enhanced Mail, proposé en 1987 avec des lignes de 64 caractères et un contrôle d'intégrité de message RSA-MD2/MD5 collé à la fin ; le nom « base64 » lui-même n'est arrivé qu'en 1993, quand les normes MIME l'ont nommé. C++ est arrivé sous le nom de C++98 en 1998 - cinq ans après le MIME - et le premier code base64 vers lequel les développeurs de la langue ont tendu la main était le duo C de 2004-2008 de Rene Nyffenegger, qu'une question de Stack Overflow du 4 décembre 2008 a répandu sur le web. La plus belle partie de cette histoire : une réponse a mis un lien vers la propre page de Nyffenegger et a repris l'implémentation, en-tête de licence et tout, et une réponse très votée a comparé les performances de sa solution face au reste du domaine. Le chant populaire a un en-tête de licence - le compositeur lui-même n'est jamais apparu dans les commentaires.

Puis l'écosystème a fait ce que les écosystèmes font. En 2002, le Boost.Serialization de Robert Ramey a livré les adapteurs d'itérateurs - le base64 le plus ancien de la boîte à outils C++, strict dans la direction de décodage et fameux pour son absence de padding dans la direction d'encodage, un an avant que la RFC 3548 ne codifie les règles d'alphabet qu'il appliquait déjà. En 2017, Boost 1.66 a apporté Beast, et avec lui le codec header-only qui est encore livré aujourd'hui avec l'attribution à Nyffenegger dans son pied de page. L'EVP_EncodeBlock d'OpenSSL et ses amis sont dans chaque version d'OpenSSL, si bien que la bête de somme est dans la boîte à outils aussi longtemps que la langue débat de savoir si le base64 devrait y être. Sous Windows, l'histoire est simplement que le système d'exploitation l'a livré : une fonction, une table de drapeaux, aucune norme impliquée du tout. Pendant ce temps, la norme elle-même est passée par C++11, C++14, C++17, C++20, C++23 (publié en 2024), et maintenant C++26, et chacune d'elles a regardé l'alphabet de 64 caractères et est passée à autre chose. Le contenu technique de C++26 s'est terminé et a été voté (114-12-3) à la réunion ISO C++ de mars 2026 à Croydon, Royaume-Uni, et il ajoute bien un nouvel en-tête <text_encoding> pour le travail de codec de texte ; les réunions suivantes du comité, en juin 2026 (Brno) et en novembre 2026 (Búzios, Brésil), ouvrent le projet de travail C++29 plutôt que de revenir sur C++26. Le base64 n'est pas dans la norme. Huit normes, trois décennies, un en-tête pour l'encodage de texte - et le comité a maintenant eu toutes les excuses possibles d'ajouter le base64, mais a refusé à chaque fois. L'histoire pratique du base64 en C++ est, et reste, l'histoire de ses bibliothèques : un duo EVP, deux saveurs Boost, un drapeau Windows, et un extrait de quarante lignes qui vous appartient.

Bizarreries qui valent la peine d'être connues

  • Le bloc de 48 octets de l'encodeur en streaming d'OpenSSL est un chiffre qui n'apparaît dans aucune RFC. Ce sont 16 groupes base64, choisis pour que la ligne de sortie fasse exactement 64 caractères - l'habitude PEM - et c'est l'un des derniers endroits où 1987 fait encore un travail porteur en 2026.
  • Le encoded_size de Boost.Beast est la section de math sous forme de fonction constexpr : 4 * ((n + 2) / 3), évaluée à la compilation quand vous lui donnez une constante. La bibliothèque standard n'a jamais eu cette ligne unique ; Boost l'a livrée dans un espace de noms detail:: à la place.
  • Le base64 avec padding le plus petit fait quatre caractères, QQ== : un octet déguisé en deux caractères. Le plus petit sans padding fait deux caractères, QQ. Le nombre de pads est aussi un message : deux pads veulent dire que le dernier groupe avait un octet, un pad veut dire qu'il en avait deux, et pas de pads veut dire qu'il en avait trois - le récepteur peut retrouver la longueur d'entrée à partir de la queue seule.
  • Les calculs de surcoût du MIME sont exacts : 4/3 fois 78/76, c'est pourquoi une pièce jointe de courriel arrive à environ 137 pour cent de sa taille d'origine, plus environ 814 octets d'en-têtes. Chaque encodeur de cet article paie le même impôt ; la largeur d'enveloppement ne change que la façon dont il est facturé.
  • Sur un libstdc++ ou MSVC typique, std::string porte les petits payloads dans un buffer de pile grâce à l'optimisation des petites chaînes au lieu d'allouer. Une entrée de 9 octets s'encode en 12 caractères et ne touche jamais le tas. La forme base64 de votre jeton peut littéralement vivre dans un cadre de pile, ce qui est le genre de repas gratuit que la bibliothèque standard ne met pas en avant.
  • La commande openssl base64 vers laquelle vous pouvez tendre la main dans un shell n'est pas une commande du tout. C'est le programme enc qui vérifie son propre nom dans argv[0] et qui change de personnalité. Un alias par comparaison de chaînes, ce qui est la façon C++ de faire les choses, en C.
  • Les identifiants de vidéo YouTube sont du base64url : onze caractères, pas de padding, pas de + ni de / aux alentours d'une URL. Le format d'encodage le plus regardé de la planète roule sur la variante « sûre pour les URL et les noms de fichiers » que la RFC 4648 a ajoutée dans une section qui tient sur une page.
  • Quatre A - AAAA - encodent trois octets nuls, parce que A est le zéro de l'alphabet. Si vous avez déjà vu un blob base64 fait entièrement d'un seul caractère, vous savez maintenant ce qu'il disait : rien.
  • Le même duo de fonctions apparaît dans les réponses à une question de Stack Overflow de 2008, dans le source de Boost.Beast avec un pied de page d'attribution, et dans les fichiers en-tête d'innombrables bases de code privées. Demandez à un développeur C++ d'où vient son base64 et la réponse la plus honnête est « je ne sais pas, et internet non plus ».

L'autre direction

Tout ce que vous venez d'emballer sera déballé par la même boîte à outils de l'autre côté, et le côté déballage a son propre lot d'habitudes : la fonction one-shot d'OpenSSL qui remplit sa queue de zéros, la correction de bug de 2025 qui a changé ce que le décodeur en streaming renvoie pour une entrée avec padding, le décodage de Boost.Beast qui s'arrête sur un caractère égaré et n'en dit jamais un mot, l'itérateur qui lève l'exception sur un seul espace, et le décodeur strict de quarante lignes qui pointe l'octet exact qui a fait mal. L'histoire complète du déballage - les tempéraments des quatre décodeurs, le transcodage base64url, les fichiers, l'habitude des 76 caractères du MIME, et les deux outils de ligne de commande qui échouent silencieusement - vit dans le guide de décodage C++ sur le site soeur. Allez le lire, puis revenez emballer quelque chose de gros. C'est tout le jeu : pas de bibliothèque standard, quatre fournisseurs aux quatre avis différents sur les retours à la ligne et les NUL, une formule qui dimensionne chaque buffer de l'article, et un impôt de 33 pour cent que chaque récepteur a droit de rembourser. Bon emballage.

Dernière mise à jour : 2026-09-08

Article associé : Décodage Base64 en C++ (Cpp) : un guide complet