Article technique

Pourquoi Excel rejette votre classeur chiffré : ECB et RC4

Vous créez un classeur, le chiffrez avec un mot de passe, transmettez le fichier à un collègue, et ce dernier l'ouvre dans Excel. Excel demande le mot de passe. Le collègue le saisit et Excel l'accepte. Jusqu'ici, le chiffrement semble correct. Puis Excel affiche une boîte de dialogue indiquant que le fichier est corrompu et ne peut pas être ouvert, ou bien s'ouvre sur une feuille remplie de cellules incompréhensibles. Le mot de passe était pourtant correct, mais le fichier reste illisible. Il s'agit du mode d'échec le plus déroutant du chiffrement Office, car la partie qui confirme le mot de passe et la partie qui contient vos données sont protégées par deux opérations différentes, et la réussite de la première ne garantit en rien celle de la seconde

Les deux pièges décrits ici ont exactement cette forme. Dans chaque cas, le vérificateur passe et le corps du fichier non, ce qui vous envoie chercher un problème de mot de passe ou de dérivation de clé qui n'existe pas. La vraie faute se trouve en aval, dans la façon dont les octets du package sont transformés. Les deux pièges sont indépendants, l'un sur le chemin AES et l'autre sur le chemin RC4, mais ils partagent un problème de diagnostic : cela vaut la peine de comprendre pourquoi un résultat à moitié correct est le plus difficile à interpréter

Pourquoi la validation du mot de passe ne prouve rien concernant le corps du fichier

Le format utilisé par les fichiers XLSX chiffrés modernes est le chiffrement standard ECMA-376 (Standard Encryption), et il stocke deux éléments chiffrés côte à côte. Le premier est l'EncryptionVerifier : un petit bloc contenant une valeur aléatoire et le hachage de cette valeur, chiffré à l'aide de la clé dérivée du mot de passe. Le second est l'EncryptedPackage : l'intégralité du conteneur ZIP du classeur, chiffrée avec cette même clé. Le vérificateur permet au lecteur de valider le mot de passe avant de décoder des mégaoctets de contenu. Décryptez le vérificateur, hachez la valeur aléatoire, comparez-la au hachage stocké, et si les deux concordent, le mot de passe est correct

Le piège réside dans le fait que le vérificateur et le package sont chiffrés par des appels distincts sur des tampons séparés. Une clé correctement dérivée décryptera correctement le vérificateur, quel que soit l'état ultérieur du package. Ainsi, si la dérivation de clé est correcte mais que la transformation du package est erronée, Excel valide le mot de passe via le vérificateur puis échoue sur le corps du fichier. Le symptôme se manifeste par « mot de passe correct, fichier corrompu », ce qui oriente l'investigation vers le traitement du mot de passe, la seule section qui fonctionnait pourtant correctement. Cette même séparation régit le cas du RC4 hérité : le hachage du vérificateur est contrôlé en premier, et un corps qui se décale n'affecte en rien cette validation initiale

Diagramme du chiffrement standard ECMA-376 tel que géré par HotXLS en Delphi : une clé dérivée protège un petit vérificateur et tout l'EncryptedPackage par des appels séparés
Le vérificateur et le paquet sont chiffrés par des appels séparés sur des tampons séparés, si bien qu'un mot de passe correct ne prouve rien quant au corps

Premier piège : AES en ECB, pas en CBC

La spécification [MS-OFFCRYPTO] §2.3.4.15 stipule que le chiffrement standard (Standard Encryption) chiffre le package avec de l'AES en mode Electronic Codebook (ECB). Chaque bloc de 16 octets du package complété est chiffré indépendamment avec la même clé. Il n'y a pas de chaînage entre les blocs et aucun vecteur d'initialisation n'est utilisé. C'est un choix inhabituel selon les critères modernes, le mode ECB étant généralement évité, mais l'interopérabilité n'est pas le lieu pour remettre en question les spécifications. Excel déchiffre le package en ECB, un producteur doit donc l'y chiffrer sous peine de non-concordance

L'erreur tentante consiste à chiffrer le package avec AES en mode CBC et un vecteur d'initialisation entièrement à zéro. Voici pourquoi cela fonctionne presque, et pourquoi « presque » est la pire des positions. En CBC, le premier bloc de texte clair est combiné par XOR avec l'IV avant chiffrement. Quand l'IV est à zéro, ce XOR ne change rien, donc le premier bloc de CBC-à-IV-zéro produit exactement le même chiffré qu'ECB. À partir du deuxième bloc, CBC réinjecte le chiffré précédent dans le suivant, donc chaque bloc après le premier diverge d'ECB

Appliquons cela à la structure. La disposition du package place un préfixe de longueur little-endian de 8 octets tout au début, donc les parties du fichier qu'Excel valide en premier se trouvent dans le premier ou les deux premiers blocs. Un premier bloc qui correspond fait passer la validation la plus précoce tandis que tous les blocs suivants se déchiffrent en bruit. Le remède n'est pas subtil une fois le mode nommé : chiffrer chaque bloc de 16 octets en ECB, sans chaînage. Dans le moteur, XlsEncryptStdPackage parcourt le buffer rembourré par pas de 16 octets et appelle AESEncryptECB128Block sur chacun, la même primitive déjà utilisée pour les blocs du vérificateur. Le source porte un commentaire à la boucle qui énonce la règle sans détour : CBC avec un IV zéro ne coïncide avec ECB que pour le premier bloc, donc le reste du package se déchiffrerait en charabia et Excel le rejetterait

Diagramme HotXLS d'AES ECB face à CBC avec IV zéro à travers les blocs de 16 octets d'un paquet XLSX chiffré, où seul le premier bloc correspond et chaque bloc suivant diverge
CBC avec un IV zéro reproduit ECB seulement pour le premier bloc, qui porte le préfixe de longueur qu'Excel valide le plus tôt, tandis que chaque bloc suivant se déchiffre en bruit
var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('report.xlsx');
    // SaveAsEncrypted sérialise le classeur, puis exécute le
    // pipeline de chiffrement standard ECMA-376 : AES-128 ECB sur le
    // package selon [MS-OFFCRYPTO] 2.3.4.15. Renvoie 1 en cas de succès.
    if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
      raise Exception.Create('Encryption failed');
  finally
    Book.Free;
  end;
end;

Second piège : la reclé RC4 se décale

Le chemin d'accès au format hérité .xls utilise le schéma RC4 CryptoAPI, et sa règle est d'une autre nature. La spécification [MS-OFFCRYPTO] §2.3.6 indique que le chiffrement est réinitialisé avec une nouvelle clé à chaque limite de bloc de 1024 octets. Le flux est divisé en blocs de 1024 octets, une nouvelle clé RC4 est dérivée pour les blocs 0, 1, 2, etc., et à l'intérieur de chaque bloc, le flux de clé est consommé en continu d'un octet à l'autre. Deux invariants doivent être respectés simultanément : renouveler la clé à chaque limite de bloc, et consommer le flux de clé sans interruption à l'intérieur d'un bloc. Le RC4 étant un chiffrement de flux, son flux de clé est une séquence ordonnée unique ; le n-ième octet extrait dépend du nombre d'octets extraits précédemment. Le déchiffrement consistant à appliquer le même XOR sur cette même séquence, le producteur et le consommateur doivent impérativement extraire les mêmes octets aux mêmes positions

C'est là toute la difficulté. Un chiffrement de flux ne propose aucune resynchronisation. Si vous gaspillez un octet de flux de chiffrement, chaque octet suivant est combiné par XOR avec le mauvais octet, et l'erreur ne se corrige jamais toute seule ; elle se propage jusqu'à la fin du bloc puis, une fois la position courante faussée, à tous les blocs suivants. Une petite erreur de comptabilité fait exactement cela. Imaginez un compteur de blocs qui démarre à la valeur sentinelle moins un, et une routine de saut qui suppose que le compteur correspond déjà au bloc courant. Partant de cette sentinelle, elle recharge la clé et consume un bloc complet de 1024 octets qui n'aurait jamais dû l'être, et fait passer le compte restant en négatif. À partir de là, le déchiffreur est décalé d'un bloc entier. Le vérificateur, contrôlé avant tout cela, passe toujours, donc le mot de passe semble bon tandis que chaque cellule de données ressort en charabia

Dans HotXLS, la logique se trouve dans TXLSDecrypterRC4. Skip et Decrypt partagent une seule boucle : recharger la clé uniquement quand la position courante franchit un nouveau bloc, l'index de bloc étant la position divisée par REKEY_BLOCK_SIZE (1024), puis consommer jusqu'au reste du bloc courant et pas plus. MakeKey est appelé avec l'index de bloc, jamais avec un index périmé ou sentinelle, et la position avance du nombre exact d'octets traités, de sorte que Skip et Decrypt restent alignés en phase avec le producteur. La leçon tient dans la plus petite unité : un octet gaspillé n'est pas une petite erreur dans un chiffrement de flux, c'est la perte totale de tout ce qui suit

Diagramme de la dérive de re-clé RC4 dans le chiffrement Excel hérité lu depuis Delphi : le producteur régénère sa clé tous les 1024 octets tandis qu'un consommateur partant de la sentinelle brûle un bloc de flux de clés et reste hors phase
Un chiffrement de flux n'a pas de resynchronisation, si bien qu'un seul bloc de flux de clés de 1024 octets brûlé laisse chaque cellule déchiffrée en charabia tandis que le vérificateur passe toujours
var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create;
  try
    // Un .xls hérité protégé par RC4 ou RC4 CryptoAPI se déchiffre dans
    // Open : passez le mot de passe et le déchiffreur de flux garde la
    // cadence de reclé alignée avec le producteur.
    Book.Open('legacy.xls', 'S3cret!');
    // lecture des cellules ici
  finally
    Book.Free;
  end;
end;

L'interopérabilité avec une spécification figée exige une correspondance à l'octet près

Les deux pièges se ramènent au même principe de fond, qui mérite d'être énoncé pour lui-même car il change la façon de peser les choix de conception. Quand le consommateur de votre sortie est un programme externe figé que vous ne pouvez pas modifier, le mode de chiffrement et la cadence de reclé ne sont pas des détails d'implémentation que vous pouvez optimiser ou simplifier. Ils font partie du contrat sur le fil. Excel déchiffrera en ECB et rechargera la clé aux frontières de 1024 octets, que ces choix vous plaisent ou non, et votre seul travail est de produire des octets qui se déchiffrent à l'original selon cette procédure exacte. Un mode plus moderne, un IV qui semble inoffensif, un compteur qui démarre là où c'est naturel : chacun de ces choix est un défaut dès qu'il diverge de ce que le lecteur attend. L'interopérabilité avec une spécification figée n'est pas approximative. Elle est exacte à l'octet près, ou elle est cassée

C'est aussi pourquoi le vérificateur ne suffit pas à valider le chiffrement. Il confirme le bon fonctionnement de la dérivation de clé, ce qui est nécessaire mais loin d'être suffisant. Un test se contentant d'ouvrir un fichier chiffré et de confirmer le mot de passe affichera un succès alors que le corps reste illisible. Un test complet décrypte le package et compare les octets récupérés à l'entrée d'origine, ou effectue un aller-retour complet de chiffrement/déchiffrement pour relire les cellules. Le vérificateur valide le mot de passe ; seul le corps valide le chiffrement

La méthode prise en charge pour lire et écrire des classeurs protégés

La surface publique est réduite. Pour écrire un classeur moderne protégé par mot de passe, remplissez ou ouvrez un TXLSXWorkbook et appelez SaveAsEncrypted avec un nom de fichier et un mot de passe ; il sérialise le classeur et exécute le pipeline de Standard Encryption décrit plus haut, en renvoyant 1 en cas de succès. SaveAsEncryptedAgile prend les mêmes arguments et écrit le schéma Agile qu'Excel 2010 et suivants utilisent par défaut, avec dérivation de clé SHA-512 et chiffrement de package AES-256. Pour lire, appelez CanReadEncrypted pour tester si un fichier est un conteneur Compound File, puis branchez : OpenEncrypted déchiffre les packages Standard et Agile et retombe sur Open pour les fichiers simples, et il lève EXlsxEncryptionNotImplemented quand le mot de passe est faux ou que le conteneur utilise un autre schéma. Un fichier .xls hérité est aussi un Compound File, il passe donc par TXLSWorkbook.Open avec le mot de passe. La gestion des modes et la boucle de reclé décrites plus haut se trouvent sous ces appels ; vous fournissez le mot de passe et le nom de fichier, et le moteur se conforme à la spécification pour vous

var
  Book: TXLSXWorkbook;
begin
  Book := TXLSXWorkbook.Create;
  try
    Book.Open('quarterly.xlsx');
    Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
    // Rouvrir côté consommateur
    Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
  finally
    Book.Free;
  end;
end;

La structure de la sortie protégée, le flux EncryptionInfo, les blocs de vérification et la disposition du package sont détaillés dans notre présentation de la génération de fichiers XLSX protégés par AES. Pour la question distincte du verrouillage des feuilles et de l'impression, consultez l'article sur la protection, la mise en page et l'impression. Ces deux aspects reposent sur le chemin de chiffrement décrit ici, intégré dans le composant HotXLS Delphi spreadsheet component pour Delphi et C++Builder, aux côtés des API de lecture, d'écriture et de rendu présentées sur ce blog