Tekninen artikkeli

Miksi Excel hylkää salatun työkirjasi: ECB ja RC4

Kirjoitat työkirjan, salaat sen salasanalla, ojennat tiedoston kollegalle, ja kollega avaa sen Excelissä. Excel kysyy salasanaa. Kollega kirjoittaa sen, ja Excel hyväksyy sen. Tähän asti salaus näyttää oikealta. Sitten Excel laittaa ylös valintaikkunan, joka sanoo, että tiedosto on vioittunut (corrupt) eikä sitä voida avata, tai se avautuu sisältäen merkityksettömiä soluja (meaningless cells). Salasana oli oikein. Tiedosto on silti rikki. Tämä on yksittäinen eniten hämmennystä herättävä vikatila (failure mode) Office-salauksessa, koska osa, joka kertoo sinulle salasanan olevan oikein, ja osa, joka pitää sisällään datasi, on suojattu kahdella eri operaatiolla, eikä toisen saaminen oikein takaa mitään toisen osalta

Molemmat tässä kuvatut bugit olivat muodoltaan juuri tällaisia. Kummassakin tapauksessa todentaja (verifier) meni läpi ja runko (body) ei, mikä lähettää sinut metsästämään salasana- tai avaimenjohtamisbugia (key-derivation bug), jota ei ole olemassa. Todellinen vika oli myöhemmässä vaiheessa (downstream), siinä, miten paketin tavut muunnettiin (transformed). Nämä kaksi vikaa ovat toisistaan riippumattomia, toinen AES-polulla ja toinen RC4-polulla, mutta niillä on yhteinen diagnoosiongelma, joten on syytä katsoa, miksi puoliksi oikea tulos on vaikein lukea

Miksi läpimenevä salasana ei todista mitään rungosta (body)

Modernin salatun XLSX:n käyttämä formaatti on ECMA-376 Standard Encryption (Standard-salaus), ja se tallentaa kaksi salattua asiaa rinnakkain. Yksi on EncryptionVerifier (salauksen todentaja): pieni lohko, joka sisältää satunnaisen arvon ja tuon arvon tiivisteen (hash), salattuna salasanasta johdetulla avaimella. Toinen on EncryptedPackage (salattu paketti): työkirjan koko zip-säilö, salattuna samalla avaimella. Todentaja (verifier) on olemassa, jotta lukija voi vahvistaa salasanan ennen kuin se käyttää vaivaa megatavuihin runkoa (body). Pura todentajan salaus, tiivistä (hash) satunnainen arvo, vertaa sitä tallennettuun tiivisteeseen, ja jos ne täsmäävät, salasana on oikea

Ansa on se, että todentaja ja paketti on salattu erillisillä kutsuilla erillisten puskureiden yli. Oikein johdettu avain purkaa todentajan salauksen oikein riippumatta siitä, mitä paketille myöhemmin tapahtuu. Joten jos avaimen johtamisesi (key derivation) on oikein, mutta paketin muunnoksesi on väärin, Excel vahvistaa salasanan todentajasta ja kaatuu sitten runkoon. Oire luetaan muodossa "oikea salasana, rikkinäinen tiedosto", mikä osoittaa tutkimuksen salasanapolkuun, joka on se ainoa osa, joka ei koskaan ollut rikki. Sama erottelu hallitsee vanhaa RC4-tapausta: todentajan tiiviste tarkistetaan ensin, ja synkronoinnista ajelehtinut runko jättää tuon tarkistuksen silti ehjäksi

Bugi yksi: AES ECB:ssä, ei CBC:ssä

[MS-OFFCRYPTO] §2.3.4.15 määrittelee, että Standard Encryption salaa paketin AES:llä Electronic Codebook (ECB) -tilassa. Jokainen tasatun (padded) paketin 16 tavun lohko salataan itsenäisesti samalla avaimella. Lohkojen välillä ei ole ketjutusta, eikä alustusvektoria (initialization vector, IV) ole. Tämä on epätavallinen valinta nykyaikaisilla standardeilla, joissa ECB:tä yleensä vältetään, mutta yhteentoimivuus ei ole paikka kyseenalaistaa (second-guess) spesifikaatiota. Excel purkaa paketin ECB:nä, joten tuottajan (producer) on salattava se ECB:nä, muuten ne kaksi eivät ole yhtä mieltä

Bugi oli siinä, että paketti salattiin AES:llä CBC-tilassa käyttäen pelkistä nollista koostuvaa alustusvektoria (all-zero initialization vector). Tässä on syy siihen, miksi se melkein toimii, ja miksi melkein on huonoin paikka mihin päätyä. CBC:ssä ensimmäinen selväkielinen lohko (plaintext block) XORataan alustusvektorin (IV) kanssa ennen salausta. Kun IV on pelkkiä nollia, tuo XOR ei muuta mitään, joten nolla-IV:llä varustetun CBC:n ensimmäinen lohko tuottaa täsmälleen saman salatekstin (ciphertext) kuin ECB. Toisesta lohkosta lähtien CBC syöttää edellisen salatekstilohkon seuraavaan, joten jokainen ensimmäisen jälkeinen lohko poikkeaa ECB:stä

Peitä tämä nyt rakenteen päälle (overlay that on the structure). Paketin asettelu asettaa 8-tavuisen little-endian-pituusliitteen (length prefix) aivan alkuun, joten ne tiedoston osat, jotka Excel tarkistaa ensimmäisenä, istuvat ensimmäisessä tai kahdessa ensimmäisessä lohkossa. Ensimmäinen lohko, joka sattuu täsmäämään, tarkoittaa, että aikaisin validointi menee läpi samalla kun jokainen myöhempi lohko puretaan (decrypts) kohinaksi. Korjaus ei ole hienovarainen, kun tila on nimetty: salaa jokainen 16-tavuinen lohko ECB:llä ja lopeta ketjuttaminen. Moottorissa (engine) XlsEncryptStdPackage kävelee tasatun (padded) puskurin läpi 16-tavuisin askelin ja kutsuu AESEncryptECB128Block-funktiota jokaiselle, mikä on sama primitiivi, jota käytetään jo todentajan (verifier) lohkoille. Lähdekoodi kantaa silmukassa (loop) kommenttia, joka esittää säännön selvästi: nolla-IV:llä varustettu CBC vastaa ECB:tä vain ensimmäisen lohkon osalta, joten loppuosa paketista purettaisiin roskaksi ja Excel hylkäisi sen

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;

Bugi kaksi: RC4-uudelleenavainnus (RC4 re-key) ajelehtii (drifts) epätahtiin

Vanha .xls-polku käyttää RC4 CryptoAPI -skeemaa, ja sen sääntö on luonteeltaan erilainen. [MS-OFFCRYPTO] §2.3.6 määrittelee, että salausavain vaihdetaan (re-keyed) jokaisen 1024 tavun lohkorajan (block boundary) kohdalla. Virta (stream) on jaettu 1024 tavun lohkoihin, uusi RC4-avain johdetaan lohkonumeroille 0, 1, 2 ja niin edelleen, ja jokaisen lohkon sisällä avainvirtaa (keystream) kulutetaan jatkuvasti tavu tavulta. Kahden invariantin on pysyttävä yhdessä: uusi avain (re-key) kullakin rajalla ja kuluta avainvirta ilman aukkoja lohkon sisällä. RC4 on virtasalaus (stream cipher), joten sen avainvirta on yksittäinen järjestetty sekvenssi; n:s tavu, jonka vedät, määräytyy sen mukaan, kuinka monta tavua olet vetänyt sitä ennen. Salauksen purku on sama XOR samaa sekvenssiä vastaan, mikä tarkoittaa, että tuottajan (producer) ja kuluttajan (consumer) on vedettävä täsmälleen samat tavut täsmälleen samoissa kohdissa (positions)

Tuo on koko vaikeus. Virtasalauksessa ei ole uudelleensynkronointia. Jos tuhlaat (waste) yhden tavun avainvirtaa, jokainen sen jälkeinen tavu XORataan väärää avainvirtatavua (keystream byte) vastaan, eikä virhe koskaan korjaa itseään; se vyöryy (cascades) lohkon loppuun ja, kun juokseva sijainti (running position) on väärin, jokaiseen sen jälkeiseen lohkoon. Bugi tässä teki juuri niin. Lohkolaskuri (block counter) alkoi vahdin (sentinel) arvosta miinus yksi, ja ohitusrutiini (skip routine) oletti laskurin jo vastaavan nykyistä lohkoa. Tuosta vahdista lähtien se uudelleenavaintaen ajoi kokonaisen 1024-tavuisen avainvirtalohkon, jota ei olisi koskaan pitänyt kuluttaa, ja siinä prosessissa se ajoi jäljellä olevan määrän negatiiviseksi. Siitä pisteestä lähtien purkaja oli täyden lohkon verran epätaihdissa (out of phase). Todentaja (verifier), joka tarkistettiin ennen mitään tästä, meni silti läpi, joten salasana näytti oikealta samalla kun jokainen datasolu tuli ulos roskana

Korjattu logiikka asuu luokassa TXLSDecrypterRC4. Sekä Skip että Decrypt jakavat yhden silmukan: uusi avain (re-key) vain silloin, kun juokseva sijainti (running position) ylittää rajan uuteen lohkoon, jossa lohkon indeksi on sijainti jaettuna arvolla REKEY_BLOCK_SIZE (1024), ja kuluta sitten enintään nykyisen lohkon jäännöksen (remainder) verran, ei enempää. MakeKey -funktiota kutsutaan lohkoindeksillä, ei koskaan vanhentuneella (stale) tai vahdin (sentinel) indeksillä, ja sijainti (position) etenee prosessoitujen tavujen tarkan määrän verran niin, että Skip ja Decrypt pysyvät vaiheittain linjassa (phase-aligned) tuottajan (producer) kanssa. Opetus istuu pienimmässä yksikössä: yksi ainoa hukattu tavu ei ole pieni virhe virtasalauksessa (stream cipher), se on kaiken sen jälkeisen (downstream) täydellinen menetys

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;

Yhteentoimivuus (interop) jäädytetyn spesifikaation (frozen spec) kanssa tarkoittaa täsmäämistä tavulleen

Molemmat bugit palautuvat samaan perusperiaatteeseen, ja se on syytä todeta erikseen, koska se muuttaa sitä, miten painotat suunnitteluvalintoja (design choices). Kun tulosteesi (output) kuluttaja (consumer) on kiinteä ulkoinen ohjelma, jota et voi muuttaa, salaustila (cipher mode) ja uudelleenavainnuksen tahti (re-key cadence) eivät ole toteutuksen yksityiskohtia, joita pääset optimoimaan tai yksinkertaistamaan. Ne ovat osa lankasopimusta (wire contract). Excel purkaa (decrypts) ECB:llä ja uusii avaimen (re-keys) 1024 tavun rajoilla riippumatta siitä, miellyttävätkö nämä valinnat sinua, ja ainoa tehtäväsi on tuottaa tavuja, jotka purkautuvat alkuperäiseen muotoon täsmälleen tuon menettelyn mukaisesti. Tila (mode), joka on nykyaikaisempi, IV (alustusvektori), joka vaikuttaa vaarattomalta, laskuri (counter), joka alkaa sieltä, missä tuntuu luonnolliselta; mikä tahansa näistä on vika heti, kun se poikkeaa siitä, mitä lukija odottaa. Yhteentoimivuus (interop) jäädytettyä spesifikaatiota (frozen specification) vastaan ei ole likimääräistä. Se on joko tavulleen tarkka (byte-exact) tai se on rikki

Tämä on myös syy siihen, miksi todentaja (verifier) on itsessään huono savutesti (smoke test). Se kertoo sinulle, että avaimen johtaminen (key derivation) toimii, mikä on välttämätöntä, mutta kaukana riittävästä. Testi, joka vain avaa salatun tiedoston ja vahvistaa salasanan menevän läpi, raportoi onnistumisesta samalla, kun runko (body) on lukukelvoton. Todellinen testi purkaa paketin ja vertaa palautettuja (recovered) tavuja alkuperäiseen syötteeseen (input) tai vie työkirjan edestakaisen matkan (round-trips) salauksen (encrypt) ja salauksenpurun (decrypt) läpi ja lukee solut takaisin. Todentaja todistaa salasanan; vain runko todistaa salauksen

Tuettu tapa lukea ja kirjoittaa suojattuja työkirjoja

Julkinen pinta (public surface) on pieni. Kirjoittaaksesi salasanasuojatun (password-protected) nykyaikaisen työkirjan, täytä tai avaa TXLSXWorkbook ja kutsu SaveAsEncrypted-metodia tiedostonimellä ja salasanalla; se serialisoi työkirjan ja suorittaa Standard Encryption -liukuhihnan (pipeline), jonka ensimmäinen korjaus korjasi, palauttaen arvon 1 onnistuessaan. Lukeaksesi, kutsu CanReadEncrypted testataksesi, onko tiedosto salattu Compound File -säilö, ja haaraudu (branch) sitten: OpenEncrypted käsittelee salattua polkua ja putoaa (falls back) takaisin Open-metodiin tavallisille tiedostoille, ja Open salasanalla on käytettävissä suoraan. Yllä kuvattu tilan (mode) käsittely ja uudelleenavainnuksen silmukka (re-key loop) istuvat näiden kutsujen alla; sinä toimitat salasanan ja tiedostonimen, ja moottori (engine) täsmää spesifikaation puolestasi

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;

Suojatun tulosteen (output) muoto, EncryptionInfo-virta (stream), todentajalohkot (verifier blocks) ja paketin asettelu käsitellään läpikäynnissämme hotxls-xlsx-aes-protected-output-delphi.html, AES-suojattu XLSX-tuloste. Erilliseen kysymykseen arkkikohtaisesta (sheet-level) lukituksesta ja siitä, miten suojaus vuorovaikuttaa sivun asetteluun ja tulostukseen, katso artikkeli hotxls-protection-page-setup-printing.html, suojaus, sivun asetus ja tulostus. Molemmat rakentuvat tässä kuvatun salauspolun varaan, joka toimitetaan osana HotXLS spreadsheet component -komponenttia Delphiä ja C++Builderia varten lukemisen, kirjoittamisen ja renderöinnin (rendering) API:den ohella, joita käsitellään muualla tässä blogissa