← Tous les articles

Base64 n'est pas du chiffrement : ce qu'il cache ou pas

Base64, c'est de l'encodage, pas du chiffrement. Pourquoi mots de passe, JWT, clés API, Basic Auth, URLs et logs restent lisibles après Base64.

« On n'a qu'à le Base64 avant de le stocker. » Si tu passes assez de fichiers de config, de dashboards internes, de bundles navigateur, de handlers de webhooks ou de code d'auth en revue, tu finis par tomber sur cette phrase. Elle semble crédible parce que la sortie a l'air brouillée :

c2VjcmV0IHBhc3N3b3Jk

Mais Base64 n'est pas du chiffrement. Ce n'est pas une protection par mot de passe. Ce n'est pas une façon sûre de cacher des clés API, des claims de JWT, des IDs utilisateur, des identifiants Basic Auth ou des données privées dans une URL. La même valeur se décode en un appel :

atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"

La règle pratique est brutale : si la valeur d'origine revient sans clé secrète, Base64 ne l'a pas protégée. Il a seulement changé la représentation.

Le test de sécurité express

Quand tu vois du Base64 dans du code, demande-toi quel travail il fait :

Base64 est utilisé pour... Bon usage ? À vérifier
Faire tenir des octets dans du JSON Oui Garder des limites de taille raisonnables
Intégrer une petite data: URI Oui Ne pas inliner de gros fichiers à l'aveugle
Stocker ciphertext, nonce, sel ou octets de signature en texte Oui Le pas cryptographique doit venir avant Base64
Porter un curseur de pagination opaque Généralement À traiter comme un détail d'interface, pas comme du contrôle d'accès
Construire un en-tête Basic Auth Seulement avec HTTPS TLS protège la requête, Base64 non
Cacher une clé API dans du code front Non Chaque navigateur reçoit à la fois la valeur et le décodeur
Stocker un mot de passe Non Utilise un hachage lent de mot de passe
Cacher des claims de JWT Non Un payload JWT signé est lisible sauf s'il est chiffré en JWE
Cacher des IDs ou des rôles dans les URLs Non L'autorisation côté serveur doit toujours passer

Si la phrase qui te vient est « pour que personne ne puisse lire », tu ne décris pas Base64. Tu décris du chiffrement, du hachage, de la signature, du contrôle d'accès ou de la gestion de secrets.

Ce que Base64 fait vraiment

Base64 est un schéma d'encodage binaire vers texte. La RFC 4648 définit l'alphabet Base64 usuel comme :

A-Z a-z 0-9 + / =

Le caractère = est du padding. Base64 groupe les octets d'entrée en blocs de 24 bits : 3 octets d'entrée deviennent 4 caractères imprimables. C'est pour ça que la sortie Base64 est environ 33 % plus grosse que les octets bruts avant compression.

btoa("hello");
// "aGVsbG8="

atob("aGVsbG8=");
// "hello"

Le même aller-retour marche depuis un terminal :

printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk

printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password

Pas de prompt de mot de passe, pas de clé privée, pas d'étape de déchiffrement, pas de sel, pas de nonce, pas d'IV, pas de tag, pas de coût de calcul. Qui détient la chaîne encodée peut l'inverser.

Base64 vs Base64url

Le Base64 standard n'est pas toujours URL-safe parce que +, / et = deviennent gênants dans les URLs, les noms de fichiers, les cookies et les segments de token. Base64url change l'alphabet :

Caractéristique Base64 standard Base64url
Caractères 62 et 63 + et / - et _
Padding Souvent = Souvent omis
Endroits classiques MIME, PEM, Basic Auth, champs binaires JWT, tokens en URL, noms de fichiers
Différence de sécurité Aucune Aucune

C'est important pour déboguer des JWT. Un JWT utilise Base64url, pas du Base64 standard avec padding :

header.payload.signature

Si tu colles un segment de JWT dans un décodeur Base64 standard et que ça échoue, la chaîne n'est pas forcément secrète ou chiffrée. Elle a juste besoin d'un traitement Base64url :

function base64urlToBase64(part) {
  let out = part.replace(/-/g, "+").replace(/_/g, "/");
  while (out.length % 4 !== 0) out += "=";
  return out;
}

Cette conversion change l'alphabet. Elle n'ajoute pas de confidentialité.

UTF-8 : le piège du navigateur

Les btoa() et atob() du navigateur travaillent sur des chaînes binaires. Les exemples ASCII pur passent bien :

btoa("hello");
// "aGVsbG8="

Mais du texte Unicode a besoin d'une étape en octets d'abord. Ça surprend les équipes qui testent avec hello et encodent plus tard des noms de client, du chinois, des emojis ou de l'arabe :

btoa("你好");
// InvalidCharacterError in browsers

Utilise des octets UTF-8 :

function utf8ToBase64(text) {
  const bytes = new TextEncoder().encode(text);
  const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
  return btoa(binary);
}

function base64ToUtf8(base64) {
  const binary = atob(base64);
  const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"

L'outil Base64 Encode & Decode de ce site fait l'aller-retour UTF-8 dans le navigateur, donc le texte non-ASCII revient bien en texte au lieu de mojibake.

Pourquoi les développeurs confondent Base64 et chiffrement

Base64 traîne souvent près de vraie sécurité :

  • Les certificats TLS sont stockés en texte PEM.
  • Les JWT utilisent des sections Base64url.
  • HTTP Basic Auth utilise Base64 pour username:password.
  • Les clés publiques SSH contiennent du matériel de clé encodé en Base64.
  • Le ciphertext chiffré est souvent encodé en Base64 avant stockage ou transport.

C'est ce dernier point qui prête le plus à confusion. Des données chiffrées peuvent être encodées en Base64, mais Base64 n'est pas le chiffrement. La sécurité est venue d'une opération cryptographique comme AES-GCM, XChaCha20-Poly1305, RSA-OAEP, JWE ou TLS. Base64 a juste rendu les octets résultants faciles à stocker en JSON, e-mail, en-tête ou colonne de base de données.

Emploie des mots précis dans le code et dans le texte produit :

encodedBackup      // OK si c'est juste du Base64
encryptedBackup    // OK seulement si un vrai chiffrement a eu lieu
signedState        // OK si un HMAC ou une signature protège l'intégrité
passwordHash       // OK si un algorithme de hachage de mot de passe l'a produit

Les noms ne sont pas cosmétiques ici. Un nom trompeur peut faire supposer à un futur relecteur qu'une frontière de sécurité existe alors qu'il n'y en a pas.

Encodage vs chiffrement vs hachage vs signature

Ces opérations résolvent des problèmes différents :

Opération Rôle principal Secret nécessaire ? Réversible ? Exemple
Encodage Changer la représentation Non Oui Base64, Base64url, URL encoding
Chiffrement Cacher le texte en clair Oui Oui, avec clé AES-GCM, XChaCha20-Poly1305, JWE
Hachage Empreinte à sens unique Non Non SHA-256 pour checksums
Hachage de mot de passe Vérification lente de mot de passe Sel plus paramètres de coût Non Argon2id, bcrypt, scrypt
Signature / MAC Prouver intégrité et origine Oui, ou paire de clés Non HMAC, JWS, signature Ed25519

Base64 répond à « cette séquence d'octets peut-elle passer par un canal texte ? ». Il ne répond pas à « qui peut lire ceci ? », « qui a modifié ceci ? » ni « cet utilisateur a-t-il le droit d'accéder à cet enregistrement ? ».

Vraie erreur de sécurité 1 : clés API cachées dans du code front

Ce motif remonte dans les apps React, les extensions de navigateur, les thèmes WordPress et les outils d'admin internes :

// La clé est quand même envoyée à chaque navigateur.
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");

fetch("https://api.example.com/report", {
  headers: { Authorization: `Bearer ${apiKey}` },
});

Base64 n'aide pas parce que le navigateur reçoit à la fois la valeur encodée et le décodeur. N'importe qui peut ouvrir DevTools, inspecter le bundle JavaScript, faire le même atob() et copier la clé.

Le fix est architectural :

  • Garde les vrais secrets sur le serveur.
  • Donne aux clés côté navigateur des scopes étroits et des restrictions par domaine.
  • Passe les appels sensibles en amont via ton backend.
  • Fais tourner tout secret déjà livré aux utilisateurs.
  • Suppose que les scanners de secrets savent décoder des chaînes qui ressemblent à du Base64.

Encoder une clé peut compliquer la relecture. Ça ne rend pas la clé plus sûre.

Vraie erreur de sécurité 2 : mots de passe stockés en Base64

C'est nocif parce que ça peut rester en silence dans une base pendant des années :

// Stockage réversible. À ne pas faire.
const storedPassword = btoa(password);

// Plus tard :
const password = atob(storedPassword);

Si la base fuite, chaque mot de passe revient tout de suite. Il n'y a aucun coût de cassage parce qu'il n'y a rien à casser.

Le stockage de mot de passe doit être à sens unique :

// Forme du flow, avec une bibliothèque de hachage de mots de passe.
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);

Un système de login normal ne devrait jamais avoir besoin de récupérer le mot de passe de l'utilisateur. Il a juste besoin de vérifier un candidat contre un hash stocké.

Vraie erreur de sécurité 3 : Basic Auth pris pour du chiffrement

L'en-tête a l'air assez encodé pour tromper :

Authorization: Basic dXNlcjpwYXNzd29yZA==

Ça décode en :

atob("dXNlcjpwYXNzd29yZA==");
// "user:password"

HTTP Basic Auth n'est acceptable que quand la requête est protégée par HTTPS. TLS chiffre l'échange HTTP en transit. Base64 se contente d'emballer username:password en une chaîne ASCII compatible en-tête.

Sur du HTTP brut, un observateur réseau peut lire les identifiants. Dans les logs, les reverse proxies, les traces APM et les dumps de support, un en-tête Basic Auth doit être traité comme un mot de passe en clair.

Vraie erreur de sécurité 4 : payloads JWT pris pour privés

Un JWT signé normal a trois sections Base64url :

header.payload.signature

Les deux premières décodent en JSON. La troisième est la signature.

eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0

Décode en :

{
  "sub": "1234",
  "role": "admin"
}

Ça ne veut pas dire que le token est faux. Un JWT signé peut être infalsifiable et rester lisible. La signature protège l'intégrité : elle dit au serveur si le token a été émis par une partie de confiance et si l'en-tête ou le payload ont bougé. Elle ne cache pas le payload.

Ne mets pas de mots de passe, de clés API, de secrets de session, de numéros de carte complets, de détails médicaux ou de données de profil privées dans le payload d'un JWT signé classique. Si des claims ont besoin de confidentialité, utilise JWE ou ne mets pas ces données dans le token.

Aussi : décoder n'est pas vérifier. Un outil de dev peut afficher les claims ; un serveur doit toujours vérifier la signature, l'issuer, l'audience, l'expiration, le key ID, la policy d'algorithme et les règles d'autorisation propres à l'application avant de leur faire confiance.

Vraie erreur de sécurité 5 : paramètres d'URL qui ont l'air opaques

Les vieilles apps passent souvent de l'état comme ça :

/profile?data=eyJ1c2VySWQiOjQyfQ==

Ça décode en :

{
  "userId": 42
}

Deux problèmes suivent. N'importe qui peut le lire, et n'importe qui peut le modifier puis le ré-encoder. Si /profile?data=... est la seule chose qui décide quel enregistrement utilisateur charger, le bug c'est l'autorisation manquante.

Patterns plus sûrs :

  • Traite les données décodées d'une URL comme de l'entrée non fiable.
  • Revérifie les permissions côté serveur.
  • Préfère une référence courte et aléatoire côté serveur quand l'état est sensible.
  • Utilise un HMAC ou une signature si le client doit transporter un état infalsifiable.
  • Garde l'état privé hors des URLs, parce que les URLs finissent copiées dans les logs, l'historique du navigateur, l'analytics et les captures d'écran de support.

Vraie erreur de sécurité 6 : exports « chiffrés » qui ne sont qu'encodés

Autre odeur de code review : une fonction comme ça :

function exportEncryptedBackup(data) {
  return btoa(JSON.stringify(data));
}

Le nom de la fonction dit chiffré. Le code dit encodé.

Si l'export est censé n'être qu'un blob texte portable, renomme-le :

function exportBase64Backup(data) {
  return btoa(JSON.stringify(data));
}

Si on dit aux utilisateurs que l'export est chiffré, utilise du vrai chiffrement authentifié et rends la gestion des clés explicite. Ça veut dire une bibliothèque de crypto éprouvée, des nonces/IVs aléatoires, des tags d'authentification, une génération de clé sûre et un plan de stockage des clés. Base64 peut toujours emballer le ciphertext ensuite.

À quoi Base64 sert vraiment

Base64 est utile. L'erreur, c'est de lui coller un rôle de sécurité.

Bons usages :

  • Données binaires dans du JSON, du XML, du HTML ou de l'e-mail.
  • Pièces jointes MIME d'e-mails.
  • Petites data: URIs pour images ou polices.
  • Mise en forme de sorties cryptographiques pour ciphertext, signatures, nonces, sels et clés publiques.
  • Segments de sérialisation compacte JWT et JOSE.
  • Curseurs de pagination opaques quand la lisibilité est une préoccupation produit, pas une frontière de sécurité.
  • Débogage de champs d'API, de payloads JWT et de blobs de config.

La formulation sûre est « encodé en Base64 », pas « chiffré ». Ce seul mot évite un nombre étonnant de malentendus en production.

Quoi utiliser à la place

Si tu as attrapé Base64 parce que tu voulais de la sécurité, choisis la primitive qui colle au boulot :

Objectif À utiliser à la place
Cacher des données à des utilisateurs ou attaquants Chiffrement authentifié comme AES-GCM ou XChaCha20-Poly1305
Protéger des données en transit HTTPS/TLS
Stocker des mots de passe Argon2id, bcrypt ou scrypt avec sel par mot de passe et coût ajusté
Stocker des clés API Gestionnaire de secrets, environnement serveur, scopes, logs d'audit et rotation
Prouver qu'une valeur n'a pas bougé HMAC ou signature numérique
Empêcher les utilisateurs d'éditer l'état d'URL Autorisation serveur plus état signé ou référence de session côté serveur
Garder des claims JWT privés JWE, ou ne pas mettre de claims privés dans le token
Mettre du binaire dans du JSON, un e-mail ou un en-tête Base64 est correct

Le dur, c'est rarement le nom de l'algorithme. C'est le threat modeling, la gestion de clés, la rotation, le contrôle d'accès, et décider qui devrait pouvoir voir le clair pour commencer.

Une checklist de code review pratique

Cherche les usages de Base64 :

rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"

Classe chaque occurrence :

  1. Transport : conversion octets vers texte pour JSON, MIME, data URI, PEM ou stockage. Généralement OK.
  2. Formatage après crypto : ciphertext, nonce, signature, sel ou clé publique. Généralement OK si la crypto est correcte.
  3. Interface opaque : curseur, code d'invitation ou état porté par le client. Vérifier l'autorisation et la protection anti-falsification.
  4. Cacher un secret : clé API, mot de passe, token, credential, claim privé ou données utilisateur. Pas OK.
  5. Nommage trompeur : variables ou textes d'UI disent chiffré, sûr, protégé, secret, masqué ou caché. Corrige le nom ou l'implémentation.

Ensuite demande-toi :

  • Est-ce qu'un navigateur, une app mobile, un utilisateur, un collecteur de logs, un proxy ou un outil de support recevrait la valeur encodée ?
  • Y a-t-il un décodeur juste à côté ?
  • Est-ce qu'un serveur vérifie les permissions après le décodage ?
  • La valeur est-elle signée si le client peut l'éditer ?
  • Si c'est un JWT, la signature est-elle vérifiée avant toute décision de confiance ?
  • Si c'est un mot de passe, pourquoi est-il réversible ?

Ça garde la review les pieds sur terre. Un PNG dans une data URI, c'est ennuyeux. Une clé API de prod dans un bundle JavaScript, non.

Réponse à incident : si tu trouves des « secrets » en Base64

Si tu découvres un secret encodé en Base64 dans du code, des logs, un ticket, de l'analytics, une URL ou un export de base :

  1. Décode-le en local pour confirmer ce que c'est.
  2. Traite la valeur décodée comme exposée.
  3. Fais tourner les credentials, tokens et clés qui ont pu être visibles.
  4. Retire la copie encodée du code, des logs, des captures ou des tickets quand c'est possible.
  5. Remplace le design par un secret côté serveur, une clé publique scoped, un état signé, du vrai chiffrement ou un hash de mot de passe.
  6. Ajoute une règle de review pour que le même motif ne revienne pas.

Ne perds pas de temps à débattre pour savoir si la chaîne Base64 était « difficile à repérer ». Les scanners automatiques, les utilisateurs du navigateur et les attaquants peuvent tous la décoder.

Quand tu as vraiment besoin d'un token chiffré

Un JWT signé standard utilise JWS : payload lisible, signature infalsifiable. Si tu as vraiment besoin de confidentialité sur les claims, utilise JWE (JSON Web Encryption).

Un JWE compact a cinq sections Base64url :

protected-header.encrypted-key.iv.ciphertext.authentication-tag

Ces sections restent du texte Base64url. La confidentialité vient de l'algorithme de chiffrement JWE et de la clé du destinataire, pas de Base64url en soi.

Dans beaucoup d'apps web, un design plus simple est meilleur : garde l'état sensible sur le serveur et envoie au navigateur un identifiant de session court et aléatoire. Tous les JWT lisibles n'ont pas besoin de devenir des JWE.

Questions fréquentes

Est-ce que Base64 est du chiffrement ?

Non. Base64 est un encodage réversible sans clé secrète. N'importe qui qui a la chaîne peut la décoder avec une fonction du navigateur, une commande de terminal, un outil en ligne ou quelques lignes de code.

Base64, c'est au moins de l'obfuscation ?

Seulement au sens le plus faible. Ça peut cacher la valeur à un regard rapide, mais ça n'arrêtera ni un développeur, ni un attaquant, ni un scanner, ni une extension de navigateur, ni un lecteur de logs, ni un outil de support.

Base64 est-il sûr pour des mots de passe ou des clés API ?

Non. Les mots de passe ont besoin d'un hachage lent comme Argon2id, bcrypt ou scrypt. Les clés API doivent rester côté serveur ou dans un gestionnaire de secrets avec scopes, audit et rotation.

Est-ce que n'importe qui peut lire un payload JWT ?

Oui, pour les JWT signés ordinaires. L'en-tête et le payload sont du JSON encodé en Base64url. La signature peut prouver l'intégrité après vérification, mais elle ne cache pas les claims.

Pourquoi Basic Auth utilise du Base64 s'il n'est pas sûr ?

Basic Auth utilise Base64 pour faire tenir username:password dans un en-tête HTTP. HTTPS/TLS fournit le chiffrement en transit. Sans HTTPS, les identifiants Basic Auth sont en pratique en clair sur le réseau.

Base64url est-il plus sûr que Base64 ?

Non. Base64url remplace + et / par - et _, et omet souvent le padding pour que la valeur passe dans les URLs et les JWT. Ça reste de l'encodage réversible.

Quoi utiliser à la place de Base64 pour chiffrer ?

Un design de chiffrement authentifié éprouvé comme AES-GCM, XChaCha20-Poly1305 ou JWE, plus une bonne génération de clés, un stockage de clés correct, de la rotation et du contrôle d'accès. Ne conçois pas ton propre format crypto.

À quoi Base64 sert-il vraiment ?

Base64 représente des octets en texte : champs binaires dans du JSON, pièces jointes d'e-mail, data URIs, fichiers PEM, signatures, formatage de ciphertext et segments de token URL-safe. C'est un emballage, pas une frontière de sécurité.

Encode et décode dans ton navigateur

Besoin d'inspecter une chaîne Base64 tout de suite ? Base64 Encode & Decode sur fixjson.org permet d'encoder ou décoder du texte localement dans le navigateur, y compris du contenu UTF-8. Sers-t'en pour inspecter des payloads JWT, des champs de réponse API ou des data: URIs, mais ne prends pas le décodage pour une vérification ou un déchiffrement.

Outils et guides liés

Sources

Dernière relecture juillet 2026.