Пишете работна книга, шифрирате я с парола, предавате файла на колега и колегата го отваря в Excel. Excel иска парола. Колегата я въвежда и Excel я приема. Дотук шифрирането изглежда правилно. След това Excel показва диалогов прозорец, който казва, че файлът е повреден и не може да бъде отворен, или се отваря с лист от безсмислени клетки. Паролата е била вярна. Файлът все пак е повреден. Това е най-объркващият режим на срив в шифрирането на Office, тъй като частта, която ви казва, че паролата е вярна, и частта, която съдържа вашите данни, са защитени от две различни операции, и правилното изпълнение на едната не гарантира нищо за другата.
И двата бъга, описани тук, имат точно тази форма. Във всеки случай верификаторът преминава успешно, а тялото не, което ви изпраща да търсите бъг в паролата или в извличането на ключове, какъвто няма. Действителната грешка е по-надолу – в начина, по който байтовете на пакета са били трансформирани. Двата дефекта са независими – един в AES пътя и един в RC4 пътя, но споделят общ проблем с диагностиката, така че си струва да се види защо полукоректният резултат е най-трудният за анализиране.
Защо преминаващата парола не доказва нищо за тялото
Форматът, който съвременният шифриран XLSX използва, е ECMA-376 Standard Encryption и съхранява две шифрирани неща едно до друго. Едното е EncryptionVerifier – малък блок, съдържащ произволна стойност и нейния хеш, шифриран с ключа, извлечен от паролата. Другото е EncryptedPackage – целият zip контейнер на работната книга, шифриран със същия ключ. Верификаторът съществува, за да може четецът да потвърди паролата, преди да изразходва усилия за мегабайти тяло. Дешифрирайте верификатора, хеширайте произволната стойност, сравнете я със съхранения хеш и ако те съвпадат, паролата е правилна.
Капанът е в това, че верификаторът и пакетът се шифрират чрез отделни извиквания над отделни буфери. Ключ, който е извлечен правилно, ще дешифрира верификатора правилно, независимо какво се случва с пакета след това. Така че, ако извличането на вашия ключ е правилно, но трансформацията на пакета е грешна, Excel потвърждава паролата от верификатора и след това се срива на тялото. Симптомът изглежда като „правилна парола, повреден файл“, което насочва разследването към пътя на паролата – единствената част, която никога не е била счупена. Същото разделение управлява наследения случай с RC4: хешът на верификатора се проверява първо, а тяло, което излиза от синхрон, все още оставя тази проверка непокътната.
Бъг едно: AES в ECB, а не в CBC
[MS-OFFCRYPTO] §2.3.4.15 указва, че Standard Encryption шифрира пакета с AES в режим Electronic Codebook. Всеки 16-байтов блок от допълнения пакет се шифрира независимо със същия ключ. Няма верижно свързване между блоковете и няма инициализиращ вектор (IV). Това е необичаен избор за съвременните стандарти, при които ECB обикновено се избягва, но съвместимостта не е място за поставяне под въпрос на спецификацията. Excel дешифрира пакета като ECB, така че софтуерът за създаване трябва да го шифрира като ECB, в противен случай двете страни няма да се разберат.
Бъгът се състоеше в това, че пакетът беше шифриран с AES в режим CBC с използване на изцяло нулев инициализиращ вектор. Ето защо това почти работи и защо „почти“ е най-лохото място, на което можете да се окажете. В CBC първият блок чист текст се XOR-ва с IV преди шифриране. Когато IV е изцяло от нули, този XOR не променя нищо, така че първият блок на CBC с нулев IV произвежда точно същия шифрован текст като ECB. От втория блок нататък CBC подава предишния блок шифрован текст в следващия, така че всеки блок след първия се отклонява от ECB.
Сега пренесете това върху структурата. Оформлението на пакета поставя 8-байтов little-endian префикс за дължина в самия старт, така че частите от файла, които Excel проверява най-рано, стоят в първия или втория блок. Първият блок, който случайно съвпада, означава, че най-ранната валидация преминава, докато всеки следващ блок се дешифрира до шум. Корекцията не е трудна, след като режимът бъде назован: шифрирайте всеки 16-байтов блок с ECB и спрете верижното свързване. В механизма XlsEncryptStdPackage обхожда допълнения буфер на стъпки от 16 байта и извиква AESEncryptECB128Block за всяка една, което е същият примитив, който вече се използва за блоковете на верификатора. Изходният код носи коментар при цикъла, който ясно заявява правилото: CBC с нулев IV съвпада с ECB само за първия блок, така че останалата част от пакета би се дешифрирала до боклук и Excel би я отхвърлил.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('report.xlsx');
// SaveAsEncrypted serializes the workbook, then runs the
// ECMA-376 Standard Encryption pipeline: AES-128 ECB over the
// package per [MS-OFFCRYPTO] 2.3.4.15. Returns 1 on success.
if Book.SaveAsEncrypted('report_secure.xlsx', 'S3cret!') <> 1 then
raise Exception.Create('Encryption failed');
finally
Book.Free;
end;
end;
Бъг две: повторното генериране на ключове за RC4 излиза от синхрон
[MS-OFFCRYPTO] §2.3.6 указва, че шифърът се регенерира с нов ключ при всяка граница на блок от 1024 байта. Потокът е разделен на блокове от 1024 байта, нов RC4 ключ се извлича за блок номер 0, 1, 2 и т.н. и във всеки блок поточното криптиране (keystream) се консумира непрекъснато от байт до байт. Два инварианта трябва да се поддържат заедно: нов ключ на всяка граница и консумиране на keystream без пропуски в рамките на даден блок. RC4 е потов шифър, така че неговият keystream е единична подредена последователност; n-тият байт, който чертаете, се определя от това колко байта сте начертали преди него. Дешифрирането е същият XOR спрямо същата последователност, което означава, че производител и потребител трябва да извличат абсолютно същите байтове на абсолютно същите позиции.
Това е цялата трудност. Поточният шифър няма повторна синхронизация. Ако изгубите един байт от keystream-а, всеки байт след него се XOR-ва срещу грешния keystream байт и грешката никога не се коригира сама; тя се пренася каскадно до края на блока и – след като текущата позиция е грешна – до всеки блок след него. Бъгът тук правеше точно това. Броячът на блокове започваше от маркер стойност минус едно и подпрограмата за пропускане предполагаше, че броячът вече съответства на текущия блок. Започвайки от този маркер, тя регенерираше ключа и изпълняваше цял 1024-байтов блок от keystream, който никога не трябваше да бъде консумиран, като по този начин правеше оставащия брой отрицателен. От този момент нататък дешифраторът беше с цял блок извън фаза. Верификаторът, проверен преди всичко това, все още преминаваше успешно, така че паролата изглеждаше правилна, докато всяка клетка с данни се превръщаше в боклук.
Коригираната логика живее в TXLSDecrypterRC4. И Skip, и Decrypt споделят един цикъл: регенериране на ключ само когато текущата позиция премине в нов блок, където индексът на блока е позицията, разделена на REKEY_BLOCK_SIZE (1024), след което консумират до остатъка от текущия блок и нищо повече. MakeKey се извиква с индекса на блока, никога с остарял или маркер индекс, а позицията напредва с точния брой обработени байтове, така че Skip и Decrypt да останат фазово подравнени с производителя. Урокът е в най-малката единица: единичен изгубен байт не е малка грешка в поточен шифър, той е пълна загуба на всичко след него.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
// CanReadEncrypted checks the Compound File (OLE2) signature so
// you can branch before attempting a normal Open. OpenEncrypted
// routes plain files to Open and handles the encrypted container.
if Book.CanReadEncrypted('legacy.xls') then
Book.OpenEncrypted('legacy.xls', 'S3cret!')
else
Book.Open('legacy.xls');
// read cells here
finally
Book.Free;
end;
end;
Съвместимостта със замразена спецификация е съпоставяне до байт
И двата бъга се свеждат до един и същ основен принцип и си струва да бъде изказан самостоятелно, тъй като променя начина, по който претегляте дизайнерските решения. Когато консуматорът на вашия изход е фиксирана външна програма, която не можете да промените, режимът на шифъра и кадансът на смяна на ключа не са подробности по имплементацията, които можете да оптимизирате или опростите. Те са част от договора за пренос. Excel ще дешифрира с ECB и ще сменя ключа на 1024-байтови граници, независимо дали тези избори ви харесват, и единствената ви задача е да произвеждате байтове, които се дешифрират до оригинала при тази точна процедура. Режим, който е по-модерен, IV, който изглежда безвреден, брояч, който започва там, където изглежда естествено – всяко от тези неща е дефект в момента, в който се отклони от очакванията на четеца. Съвместимостта спрямо замразена спецификация не е приблизителна. Тя е байтово точна или е счупена.
Ето защо и верификаторът сам по себе си е слаб тест. Той ви казва, че извличането на ключа работи, което е необходимо, но далеч не е достатъчно. Тест, който само отваря шифриран файл и потвърждава паролата, ще отчете успех, докато тялото е неразчетимо. Истинският тест дешифрира пакета и сравнява възстановените байтове с оригиналните входни данни или превърта работна книга чрез шифриране и дешифриране и чете клетките обратно. Верификаторът доказва паролата; само тялото доказва шифрирането.
Поддържаният начин за четене и запис на защитени работни книги
Публичната повърхност е малка. За да запишете защитена с парола съвременна работна книга, попълнете или отворете TXLSXWorkbook и извикайте SaveAsEncrypted с име на файл и парола; това сериализира работната книга и стартира тръбопровода за Standard Encryption, който първата корекция поправи, връщайки 1 при успех. За четене извикайте CanReadEncrypted, за да тествате дали файлът е шифриран Compound File контейнер, след което се разклонете: OpenEncrypted управлява шифрирания път и се връща към Open за обикновени файлове, а Open с парола е наличен директно. Управлението на режима и цикълът за смяна на ключа, описани по-горе, се намират под тези извиквания; вие предоставяте паролата и името на файла, а механизмът отговаря на спецификацията от ваше име.
var
Book: TXLSXWorkbook;
begin
Book := TXLSXWorkbook.Create(nil);
try
Book.Open('quarterly.xlsx');
Book.SaveAsEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
// Reopen on the consumer side
Book.OpenEncrypted('quarterly_locked.xlsx', 'P@ssphrase');
finally
Book.Free;
end;
end;
Формата на защитения изход, потокът EncryptionInfo, блоковете на верификатора и оформлението на пакета са разгледани в нашето ръководство за защитен с AES XLSX изход. За отделния въпрос относно заключването на ниво лист и как защитата си взаимодейства с настройките на страницата и печата, вижте статията за защита, настройки на страницата и печат. И двете се базират на пътя на шифриране, описан тук, който се доставя като част от HotXLS spreadsheet component за Delphi и C++Builder, заедно с API за четене, запис и рендиране, разгледани на други места в този блог.