HotXLS lit les fichiers Excel chiffrés en mode Agile — la protection par mot de passe appliquée par défaut par Excel 2010 et toutes les versions ultérieures — via un seul appel : TXLSXWorkbook.OpenEncrypted. Le composant analyse le descripteur de chiffrement XML, dérive les clés du mot de passe avec une chaîne de hachage SHA-512 par spin-count, vérifie le mot de passe par rapport au vérificateur chiffré, puis déchiffre le package par segments AES-CBC de 4096 octets. Aucun Excel installé, aucun COM, aucun DLL de cryptographie externe n'est requis
Cet article traite spécifiquement de la lecture du chiffrement Agile. Deux problèmes connexes font l'objet de leurs propres articles : l'interopérabilité avec les anciens schémas RC4 et XOR dans les anciens fichiers BIFF .xls est traitée dans l'article sur l'interopérabilité ECB et RC4, et la création de classeurs protégés par mot de passe avec le chiffrement standard ECMA-376 est abordée dans l'article sur la génération de fichiers XLSX protégés par AES. Ici, le fichier existe déjà, quelqu'un d'autre l'a chiffré, et votre tâche consiste à l'ouvrir
Le scénario qui impose cela est familier pour quiconque gère un pipeline de documents. Un service d'importation côté serveur accepte les chargements de classeurs ; il n'y a pas d'Excel sur la machine et il n'y en aura jamais ; et un matin, un client télécharge un fichier .xlsx tout à fait ordinaire que le lecteur ZIP rejette parce qu'il ne s'agit pas du tout d'un ZIP. Le client l'a enregistré avec un mot de passe. À partir de ce moment, soit votre chargeur comprend [MS-OFFCRYPTO], soit il renvoie le fichier à un utilisateur qui, de son point de vue, n'a rien fait d'inhabituel
Qu'est-ce que le chiffrement Agile dans un fichier Excel ?
Le chiffrement Agile est le schéma de protection par mot de passe défini dans [MS-OFFCRYPTO] §2.3.4.10 à §2.3.4.15, et c'est ce qu'Excel 2010 et les versions ultérieures écrivent chaque fois qu'un classeur est enregistré avec un mot de passe. Le fichier chiffré n'est plus un package ZIP. C'est un conteneur OLE Compound File Binary (CFB) contenant deux flux : EncryptionInfo, qui décrit comment le chiffrement a été effectué, et EncryptedPackage, qui est le véritable ZIP .xlsx chiffré sous forme de blob opaque. La signature CFB (D0 CF 11 E0 A1 B1 1A E1) est la même signature magique que portent les fichiers BIFF .xls hérités, c'est pourquoi un fichier renommé ou chiffré ne peut pas être classé uniquement par son extension
Ce qui distingue le mode Agile de ses prédécesseurs, c'est que EncryptionInfo s'auto-décrit. Après un préfixe de version de 8 octets, les versions majeure et mineure étant toutes deux à 4, le flux est un descripteur XML UTF-8. Un élément keyData déclare le chiffrement (AES), le mode de chaînage (ChainingModeCBC), le hachage (SHA512), la longueur de la clé en bits, la taille du bloc et un sel Base64. Un élément de mot de passe keyEncryptor porte son propre sel, le spinCount et trois charges utiles Base64 : encryptedVerifierHashInput, encryptedVerifierHashValue et encryptedKeyValue. Excel écrit de l'AES-256 avec un spin count de 100 000, mais le descripteur est autorisé à déclarer de l'AES-128 ou de l'AES-192, et HotXLS respecte la valeur de keyBits déclarée plutôt que de supposer du 256
Un point d'entrée unique pour les classeurs en texte brut, Standard et Agile
La méthode TXLSXWorkbook.OpenEncrypted gère les trois états qu'un appelant peut rencontrer : ZIP brut, chiffrement Standard et chiffrement Agile, de sorte que les gestionnaires de chargement n'ont pas besoin de classer les fichiers avant de les charger. La méthode commence par analyser le fichier : s'il n'y a pas de signature CFB, elle se tourne vers le chemin normal de Open et le mot de passe est simplement ignoré. Si le fichier est un conteneur CFB, elle tente d'abord le chiffrement standard ECMA-376 et, lorsque la signature de version d'EncryptionInfo est l'Agile 4.4, elle bascule vers le pipeline Agile. La valeur de retour est 1 en cas de succès, le même contrat que Open
var
Wb: TXLSXWorkbook;
begin
Wb := TXLSXWorkbook.Create;
try
// Works for plain .xlsx, Standard-encrypted and
// Agile-encrypted files alike
if Wb.OpenEncrypted('upload.xlsx', 'customer-password') = 1 then
Writeln(VarToWideStr(Wb.Sheets[1].Cells[1, 1].Value));
finally
Wb.Free;
end;
end;
Le repli vers l'entrée non chiffrée est plus important qu'il n'y paraît. Un importateur par lots qui appelle toujours OpenEncrypted n'a pas besoin de branchement conditionnel au niveau de l'appel : les fichiers qui n'ont jamais été protégés se chargent exactement comme avant, et les fichiers qui arrivent chiffrés sont déchiffrés sur place puis transmis au chargeur ZIP ordinaire sous forme de flux en mémoire. Il n'y a qu'un seul chemin de code à tester, pas trois
Comment un mot de passe devient-il une clé AES ?
Le chiffrement Agile n'utilise jamais le mot de passe directement. HotXLS calcule d'abord un hachage itéré : le condensé initial est SHA-512 sur le sel du mot de passe concaténé avec les octets UTF-16LE du mot de passe, puis le condensé est re-haché spinCount fois, chaque cycle ajoutant le compteur d'itérations petit-boutiste (little-endian) 32 bits devant le condensé précédent. Avec le spin count par défaut de 100 000 d'Excel, cela représente cent mille invocations SHA-512 consécutives par tentative de mot de passe, ce qui est le but recherché. Le spin count est un frein contre les attaques par force brute : il coûte quelques millisecondes à un appelant légitime, et coûte ces mêmes millisecondes à un attaquant par dictionnaire pour chaque tentative de devinette
// [MS-OFFCRYPTO] iterated password hash:
// H(0) = SHA-512(salt + UTF-16LE(password))
// H(n) = SHA-512(LE32(n - 1) + H(n - 1)), repeated spinCount times
function AgilePasswordHash(const Password: WideString;
const Salt: TBytes; SpinCount: Integer): TBytes;
var
buf: TBytes;
i: Integer;
begin
Result := XlsSHA512(Concat(Salt, Utf16LEBytes(Password)));
SetLength(buf, 4 + 64);
for i := 0 to SpinCount - 1 do
begin
PutLE32(buf, 0, i); // iteration counter, little-endian
Move(Result[0], buf[4], 64); // previous digest
Result := XlsSHA512(buf);
end;
end;
Le hachage obtenu n'est pas encore une clé. Trois clés distinctes en sont dérivées en le hachant une fois de plus après avoir ajouté une clé de bloc fixe de 8 octets, une constante par objectif : FE A7 D2 76 3B 4B 9E 79 pour déchiffrer l'entrée du vérificateur, D7 AA 0F 6D 30 61 34 4E pour le hachage du vérificateur et 14 6E 0B E7 AB AC D0 D6 pour déballer la véritable clé du package. Chaque résultat SHA-512 est tronqué à la longueur de clé déclarée, et, selon [MS-OFFCRYPTO], complété par des octets 0x36 dans le cas théorique où le hachage serait plus court que la clé. La même règle de complétion (padding) 0x36 s'applique lorsque le sel du mot de passe est étendu à la taille du bloc pour servir de vecteur d'initialisation CBC
La vérification du mot de passe et le piège de la troncature à saltSize
HotXLS vérifie le mot de passe avant de toucher au package, using la paire de vérification du descripteur. Il déchiffre encryptedVerifierHashInput avec la première clé dérivée, hache le résultat avec SHA-512, déchiffre encryptedVerifierHashValue avec la deuxième clé dérivée et compare les deux condensés octet par octet. Un écart signifie que le mot de passe est incorrect, ce qui est signalé comme un résultat distinct plutôt que comme un classeur corrompu. De plus, cela garantit que le corps du package n'est jamais déchiffré avec une mauvaise clé, évitant ainsi qu'un mot de passe incorrect ne produise des données corrompues à l'apparence trompeuse
Il y a ici un détail de spécification facile à manquer. [MS-OFFCRYPTO] §2.3.4.13 définit le vérificateur comme contenant saltSize octets de données aléatoires, où saltSize est la longueur du sel du chiffreur de clé, et non la taille du bloc de chiffrement. Le texte chiffré AES-CBC étant aligné sur les blocs, l'entrée décryptée du vérificateur revient complétée pour atteindre un multiple de 16 octets, et elle doit être tronquée à la taille saltSize avant le hachage. Excel écrit toujours une valeur saltSize égale à la taille blockSize, toutes deux à 16, ainsi une implémentation qui ignorerait la troncature passerait tous les tests sur les fichiers réels générés par Excel, puis échouerait sur le premier fichier d'un producteur ayant choisi une longueur de sel différente. HotXLS effectue la troncature selon la longueur de sel déclarée car c'est ce que la spécification exige, et la concordance des deux valeurs dans la pratique est une coïncidence, pas une règle
Comment le package chiffré (EncryptedPackage) est-il déchiffré ?
Le flux EncryptedPackage commence par une taille de texte brut sur 8 octets en petit-boutiste, suivie du texte chiffré par segments de 4096 octets, et HotXLS le déchiffre segment par segment avec un IV propre à chaque segment. La clé de package elle-même n'est pas dérivée du mot de passe : c'est une clé intermédiaire aléatoire que le programme d'écriture a chiffrée dans encryptedKeyValue, et HotXLS la déballe avec la troisième clé dérivée, en la tronquant à la longueur de clé déclarée par keyData. L'IV de chaque segment est SHA-512 sur le sel de keyData concaténé avec l'index de segment 32 bits petit-boutiste, tronqué à la taille du bloc. Cette construction permet de déchiffrer indépendamment n'importe quel segment de 4096 octets, ce qui rend théoriquement le format adapté à l'accès aléatoire, bien que HotXLS déchiffre l'intégralité du package en mémoire avant de transmettre les octets ZIP résultants à son chargeur XLSX habituel
La taille de texte brut déclarée termine le travail. La sortie AES-CBC étant alignée sur le bloc, le dernier segment contient jusqu'à 15 octets de complétion (padding) qui ne font pas partie du document ; le tampon déchiffré est tronqué selon le préfixe de taille, et le résultat est exactement le ZIP .xlsx chiffré par Excel. HotXLS valide le préfixe par rapport à la longueur réelle du flux avant de déchiffrer, ainsi un fichier tronqué ou un champ de taille altéré échoue proprement au lieu de déborder
Signalement des erreurs et limites honnêtes
Les modes d'échec sont délibérément séparés. Un mot de passe incorrect lève une exception contenant un message explicite, généré par l'écart du vérificateur, afin qu'une interface utilisateur puisse inviter l'utilisateur à réessayer. Un conteneur CFB dont le descripteur déclare des algorithmes hors des formats pris en charge — tout ce qui diffère de l'AES en mode CBC et du hachage SHA-512 dans un descripteur Agile, ou un conteneur qui n'est ni Standard ni Agile — lève une exception différente identifiant le schéma comme non pris en charge. Les deux cas ne doivent jamais être confondus : réessayer un mot de passe contre un schéma non pris en charge fait perdre du temps à l'utilisateur, et signaler un mot de passe incorrect comme une erreur de format oriente votre équipe d'assistance dans la mauvaise direction
function LoadUploadedWorkbook(const FileName: WideString;
const Password: WideString; Wb: TXLSXWorkbook): Boolean;
begin
Result := False;
try
Result := Wb.OpenEncrypted(FileName, Password) = 1;
except
on E: EXlsxEncryptionNotImplemented do
// Raised for both a wrong password and an unsupported
// scheme; E.Message states which, so log it verbatim and
// only offer a password retry for the wrong-password case
RejectUpload(FileName, E.Message);
end;
end;
Les limites méritent d'être énoncées clairement. HotXLS lit les descripteurs Agile déclarant de l'AES en mode CBC avec SHA-512, ce qui couvre ce qu'Excel 2010 à Excel 365 écrivent réellement, dans les trois tailles de clé. Les descripteurs déclarant d'autres chiffrements ou algorithmes de hachage sont rejetés au lieu d'être devinés, et les chiffreurs de clé basés sur des certificats ne sont pas consultés, seul le chiffreur de clé par mot de passe l'est. Côté écriture, HotXLS produit actuellement le chiffrement standard plutôt qu'Agile, une distinction qui compte si les outils en aval inspectent le schéma ; les détails se trouvent dans l'article sur la génération de fichiers XLSX protégés par AES
Les chargements protégés par mot de passe cessent d'être un cas particulier dès lors que le chargeur treats le chiffrement comme une partie intégrante du format de fichier et non comme une exception. Le point d'entrée OpenEncrypted, la dérivation SHA-512 par spin-count et le pipeline segmenté AES-CBC décrits ici font partie de la bibliothèque HotXLS Delphi Excel Component, aux côtés du reste de son moteur natif de lecture et d'écriture XLS et XLSX pour Delphi et C++Builder