Techninis straipsnis

Agile šifravimu apsaugotų Excel failų skaitymas Delphi aplinkoje su HotXLS

„HotXLS“ nuskaito „Agile“ šifravimu apsaugotus „Excel“ failus – slaptažodžio apsaugą, kurią „Excel 2010“ ir visos vėlesnės versijos taiko pagal numatytuosius nustatymus – vienu iškvietimu: TXLSXWorkbook.OpenEncrypted. Komponentas išanalizuoja XML šifravimo aprašą, išveda raktus iš slaptažodžio naudodamas SHA-512 sukimosi skaičiaus (spin-count) maišos grandinę, patikrina slaptažodį pagal užšifruotą tikrintuvą, o tada iššifruoja paketą 4096 baitų AES-CBC segmentais. Čia nenaudojamas joks „Excel“ diegimas, jokios COM paslaugos ar išorinės kriptografinės DLL bibliotekos

Šiame straipsnyje konkrečiai aptariama „Agile“ šifravimo skaitymo pusė. Dvi susijusios problemos turi savo straipsnius: sąveika su pasenusiomis RC4 ir XOR schemomis senuose BIFF .xls failuose aptariama ECB ir RC4 sąveikos straipsnyje, o slaptažodžiu apsaugotų darbo knygų kūrimas su ECMA-376 standartiniu šifravimu aprašomas straipsnyje apie AES apsaugotą XLSX išvestį. Čia failas jau egzistuoja, jį užšifravo kažkas kitas, o jūsų darbas – jį atidaryti

Scenarijus, kuris verčia spręsti šią problemą, yra pažįstamas visiems, valdantiems dokumentų apdorojimo srautus. Serverio pusėje veikianti importavimo tarnyba priima įkeliamas darbo knygas; mašinoje nėra ir niekada nebus įdiegtos „Excel“ programos; ir vieną rytą klientas įkelia visiškai įprastą .xlsx failą, kurį ZIP skaitytuvas atmeta, nes tai išvis nėra ZIP failas. Klientas jį išsaugojo su slaptažodžiu. Nuo to momento jūsų įkėliklis turi suprasti [MS-OFFCRYPTO] arba grąžinti failą vartotojui, kuris, savo požiūriu, nepadarė nieko neįprasto

Kas yra Agile šifravimas Excel faile?

„Agile“ šifravimas yra slaptažodžio apsaugos schema, apibrėžta [MS-OFFCRYPTO] §2.3.4.10 – §2.3.4.15, ir būtent ją „Excel 2010“ ir vėlesnės versijos įrašo kaskart, kai darbo knyga išsaugoma su slaptažodžiu. Užšifruotas failas nebėra ZIP paketas. Tai yra „OLE Compound File Binary“ (CFB) konteineris, turintis du srautus: EncryptionInfo, kuris aprašo, kaip buvo atliktas šifravimas, ir EncryptedPackage, kuris yra tikrasis .xlsx ZIP failas, užšifruotas kaip nepermatomas blokas (blob). CFB parašas (D0 CF 11 E0 A1 B1 1A E1) is ta pati magiška reikšmė, kurią turi ir pasenę BIFF .xls failai, todėl pervadinto ar užšifruoto failo negalima identifikuoti vien pagal jo plėtinį

„Agile“ šifravimą nuo jo pirmtakų skiria tai, kad EncryptionInfo aprašo save pats. Po 8 baitų versijos prefikso, kur tiek pagrindinė (major) tiek šalutinė (minor) versija yra 4, srautas yra UTF-8 XML aprašas. Elementas keyData deklaruoja šifrą (AES), grandininį režimą (ChainingModeCBC), maišą (SHA512), rakto ilgį bitais, bloko dydį ir Base64 druską (salt). Slaptažodžio elementas keyEncryptor turi savo druską, spinCount ir tris Base64 duomenis: encryptedVerifierHashInput, encryptedVerifierHashValue ir encryptedKeyValue. „Excel“ rašo AES-256 su 100 000 sukimų skaičiumi, tačiau apraše leidžiama deklaruoti AES-128 arba AES-192, ir „HotXLS“ atsižvelgia į tai, ką nurodo keyBits, užuot prisiėmusi reikšmę 256

Vienas įėjimo taškas tekstinėms, standartinėms ir „Agile“ darbo knygoms

TXLSXWorkbook.OpenEncrypted apdoroja visas tris būsenas, kurias gali sutikti kviečiantysis: paprastą ZIP, standartiniu būdu užšifruotą bei „Agile“ užšifruotą failą, todėl įkėlimo apdorojimo programoms nereikia klasifikuoti failų prieš juos įkeliant. Metodas pirmiausia patikrina failą: jei nėra CFB parašo, jis nukreipia į įprastą Open kelią, o slaptažodis tiesiog ignoruojamas. Jei failas yra CFB konteineris, jis pirmiausia bando ECMA-376 standartinį šifravimą, o kai EncryptionInfo versijos parašas yra Agile 4.4, nukreipia į „Agile“ srautą. Sėkmės atveju grąžinama reikšmė 1 – tokia pati kaip ir Open sutartis

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;

Paprasto failo palaikymas (fallback) yra svarbesnis nei atrodo. Imtuvui, kuris visada naudoja OpenEncrypted, nereikia šakotumo iškvietimo vietoje: failai, kurie niekada nebuvo apsaugoti, įkeliami tiksliai taip pat kaip ir anksčiau, o užšifruoti failai iššifruojami vietoje ir perduodami įprastam ZIP įkėlikliui kaip srautas atmintyje. Yra tik vienas kodo kelias testavimui, o ne trys

Kaip slaptažodis tampa AES raktu?

„Agile“ šifravimas niekada nenaudoja slaptažodžio tiesiogiai. „HotXLS“ pirmiausia apskaičiuoja iteracinę maišą: pradinė santrauka yra SHA-512, apskaičiuota iš slaptažodžio druskos, sujungtos su slaptažodžio UTF-16LE baitais, o po to santrauka perskaičiuojama spinCount kartų, kiekviename raunde prie ankstesnės santraukos pridedant 32 bitų mažo galo (little-endian) iteracijos skaitiklį. Su numatytuoju „Excel“ 100 000 sukimų skaičiumi tai yra šimtas tūkstančių nuoseklių SHA-512 iškvietimų vienam bandymui įvesti slaptažodį, ir tai yra visa esmė. Sukimų skaičius yra apsauga nuo brutalių atakų: teisėtam vartotojui tai kainuoja kelias milisekundes vieną kartą, o žodyno atakos vykdytojui – tas pačias kelias milisekundes kiekvienam spėjimui

// [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;

Persukta maiša vis dar nėra raktas. Iš jos išvedami trys atskiri raktai, dar kartą apskaičiuojant maišą su pridėtu fiksuotu 8 baitų bloko raktu, kuris skiriasi priklausomai nuo tikslo: FE A7 D2 76 3B 4B 9E 79 tikrintuvo įvesties iššifravimui, D7 AA 0F 6D 30 61 34 4E tikrintuvo maišai ir 14 6E 0B E7 AB AC D0 D6 tikrajam paketo raktui išpakuoti. Kiekvienas SHA-512 rezultatas sutrumpinamas iki deklaruoto rakto ilgio, o pagal [MS-OFFCRYPTO], teoriniu atveju, kai maiša yra trumpesnė už raktą, užpildomas 0x36 baitais. Ta pati užpildymo taisyklė 0x36 taikoma, kai slaptažodžio druska pratęsiama iki bloko dydžio, kad būtų naudojama kaip CBC inicializavimo vektorius (IV)

Slaptažodžio tikrinimas ir druskos dydžio (saltSize) sutrumpinimo spąstai

„HotXLS“ patikrina slaptažodį prieš paliesdama paketą, naudodama tikrintuvų porą iš aprašo. Ji iššifruoja encryptedVerifierHashInput su pirmuoju išvestu raktu, gautą rezultatą maišo su SHA-512, iššifruoja encryptedVerifierHashValue su antruoju išvestu raktu ir palygina abi santraukas baitas po baito. Nesutapimas reiškia, kad slaptažodis yra neteisingas – tai pranešama kaip atskiras rezultatas, o ne kaip sugadinta darbo knyga. Svarbiausia, kad paketo turinys niekada neiššifruojamas su blogu raktu, todėl nekyla pavojaus, kad įvedus neteisingą slaptažodį bus sugeneruoti apgaulingai atrodantys sugadinti duomenys

Čia yra specifikacijos detalė, kurią lengva suprasti klaidingai. [MS-OFFCRYPTO] §2.3.4.13 apibrėžia tikrintuvą kaip saltSize baitų atsitiktinių duomenų, kur saltSize yra rakto šifruotojo druskos ilgis, o ne šifro bloko dydis. Kadangi AES-CBC šifruotas tekstas yra lygiavertis blokui, iššifruota tikrintuvo įvestis grąžinama užpildyta iki 16 baitų kartotinio, todėl prieš maišos skaičiavimą ji turi būti sutrumpinta iki saltSize ilgio. „Excel“ visada rašo saltSize, lygų blockSize (abu 16), todėl įgyvendinimas, kuris praleidžia šį sutrumpinimą, praeina visus testus su tikra „Excel“ išvestimi, tačiau sugenda pirmajame faile iš kūrėjo, kuris pasirinko kitokį druskos ilgį. „HotXLS“ sutrumpina duomenis iki druskos ilgio, nes būtent taip reikalauja specifikacija, o šių dviejų reikšmių sutapimas praktikoje yra tik sutapimas, o ne taisyklė

Kaip iššifruojamas „EncryptedPackage“?

„EncryptedPackage“ srautas prasideda nuo 8 baitų mažo galo plaintext dydžio, po kurio seka šifruotas tekstas 4096 baitų segmentais, ir „HotXLS“ jį iššifruoja segmentas po segmento su nauju IV kiekvienam segmentui. Paties paketo raktas nėra išvestas iš slaptažodžio: tai atsitiktinis tarpinis raktas, kurį rašytuvas užšifravo encryptedKeyValue reikšmėje, ir „HotXLS“ jį išpakuoja su trečiuoju išvestu raktu, sutrumpindama iki keyData deklaruoto rakto ilgio. Kiekvieno segmento IV yra SHA-512, apskaičiuotas iš keyData druskos, sujungtos su 32 bitų mažo galo segmento indeksu, sutrumpintas iki bloko dydžio. Tokia konstrukcija reiškia, kad bet kuris 4096 baitų segmentas gali būti iššifruotas nepriklausomai, o tai iš esmės daro formatą tinkamą atsitiktinei prieigai, nors „HotXLS“ iššifruoja visą paketą į atmintį ir gautus ZIP baitus perduoda įprastam XLSX įkėlikliui

Deklaruotas plaintext dydis atlieka paskutinį darbą. AES-CBC išvestis yra sulygiuota su bloku, todėl paskutinis segmentas turi iki 15 baitų užpildymo, kuris nėra dokumento dalis; iššifruotas buferis sutrumpinamas iki dydžio prefikso, o rezultatas yra tiksliai tas .xlsx ZIP failas, kurį užšifravo „Excel“. „HotXLS“ patikrina prefiksą su tikruoju srauto ilgiu prieš iššifravimą, todėl nebaigtas įkėlimas ar suklastotas dydžio laukas sugenda švariai, o ne sukelia buferio perpildymą

Klaidų pranešimai ir sąžiningos ribos

Nesėkmių režimai sąmoningai laikomi atskirai. Neteisingas slaptažodis sukelia išimtį su aiškiu pranešimu apie neteisingą slaptažodį, kurį nulemia tikrintuvo nesutapimas, todėl naudotojo sąsaja gali paraginti vartotoją bandyti dar kartą. CFB konteineris, kurio apraše deklaruojami nepalaikomi algoritmai – bet kas, išskyrus AES su CBC grandine bei SHA-512 maiša „Agile“ apraše, arba konteineris, kuris nėra nei „Standard“, nei „Agile“ – sukelia kitą išimtį, identifikuojančią schemą kaip nepalaikomą. Šie du dalykai niekada neturi būti painiojami: bandymas įvesti slaptažodį iš naujo prieš nepalaikomą schemą eikvoja vartotojo laiką, o pranešimas apie neteisingą slaptažodį kaip apie formato klaidą nukreipia palaikymo komandą klaidingu keliu

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;

Ribas verta įvardyti aiškiai. „HotXLS“ nuskaito „Agile“ aprašus, kurie deklaruoja AES CBC režimu su SHA-512, zodžiu, tai apima viską, ką „Excel 2010“ – „Excel 365“ iš tikrųjų įrašo visais trimis raktų dydžiais. Aprašai, deklaruojantys kitus šifrus ar maišos algoritmus, atmetami, užuot spėliojus, o sertifikatais pagrįsti raktų šifruotuvai nėra naudojami – palaikomas tik slaptažodžio rakto šifruotuvas. Rašymo pusėje „HotXLS“ šiuo metu sukuria standartinį šifravimą (Standard Encryption), o ne „Agile“; šis skirtumas yra svarbus, jei vėlesni įrankiai tikrina schemą. Detalės pateikiamos straipsnyje apie AES apsaugotos XLSX išvesties kūrimą

Slaptažodžiu apsaugoti įkeliami failai nustoja būti išimtiniu atveju, kai įkėliklis šifravimą traktuoja kaip failo formato dalį, o ne kaip jo išimtį. OpenEncrypted įėjimo taškas, SHA-512 sukimų skaičiaus išvedimas ir čia aprašytas srautas pateikiami kaip „HotXLS Delphi Excel Component“ dalis kartu su likusiu vietiniu XLS bei XLSX skaitymo ir rašymo varikliu, skirtu Delphi bei C++Builder