Kirjoitat työkirjan, salaat sen salasanalla, annat 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 näyttää valintaikkunan, jossa sanotaan tiedoston olevan vioittunut eikä sitä voida avata, tai se avautuu merkityksettömien solujen taulukkoon. Salasana oli oikein. Tiedosto on silti rikki. Tämä on hämmentävin epäonnistumistila Office-salauksessa, koska osa, joka kertoo salasanan olevan oikein, ja osa, joka sisältää tietosi, on suojattu kahdella eri toiminnolla, eikä toisen oikeellisuus takaa mitään toisesta.
Molemmilla tässä kuvatuilla virheillä oli täsmälleen tämä muoto. Kummassakin tapauksessa todennus (verifier) meni läpi ja runko ei, mikä lähettää sinut etsimään salasana- tai avaimenjohtamisvirhettä, jota ei ole olemassa. Todellinen vika oli alavirrassa, siinä miten paketin tavut muunnettiin. Nämä kaksi vikaa ovat itsenäisiä - yksi AES-polulla ja yksi RC4-polulla - mutta ne jakavat diagnosointiongelman, joten on syytä nähdä, miksi puoliksi oikea tulos on kaikkein vaikein tulkittava.
Miksi läpäisevä salasana ei todista mitään rungosta
Modernin salatun XLSX-tiedoston käyttämä formaatti on ECMA-376 Standard Encryption, ja se tallentaa kaksi salattua asiaa rinnakkain. Yksi on EncryptionVerifier: pieni lohko, joka pitää sisällään satunnaisen arvon ja kyseisen arvon tiivisteen (hash), salattuna salasanasta johdetulla avaimella. Toinen on EncryptedPackage: työkirjan koko zip-säiliö, salattuna samalla avaimella. Todennus on olemassa, jotta lukija voi vahvistaa salasanan ennen kuin se kuluttaa vaivaa megatavuiseen runkoon. Pura todennuksen salaus, tiivistä satunnainen arvo, vertaa sitä tallennettuun tiivisteeseen, ja jos ne täsmäävät, salasana on oikein.
Ansa on siinä, että todennus ja paketti salataan erillisillä kutsuilla erillisten puskurien yli. Oikein johdettu avain purkaa todennuksen oikein riippumatta siitä, mitä paketille tapahtuu sen jälkeen. Joten jos avaimen johtaminen on oikein mutta paketin muunnos on väärin, Excel vahvistaa salasanan todennuksesta ja epäonnistuu sitten rungon kohdalla. Oire luetaan muodossa "oikea salasana, rikkinäinen tiedosto", mikä ohjaa tutkimuksen salasanapolulle, joka oli se ainoa osa, joka ei koskaan ollut rikki. Sama erottelu hallitsee perinteistä RC4-tapausta: todennustiiviste tarkistetaan ensin, ja synkronoinnista poikkeava runko jättää silti kyseisen tarkistuksen ennalleen.
AES ECB-tilassa, ei CBC-tilassa
Dokumentti [MS-OFFCRYPTO] §2.3.4.15 määrittelee, että Standard Encryption salaa paketin AES-salauksella Electronic Codebook (ECB) -tilassa. Puskuroidun paketin jokainen 16-tavuinen lohko salataan itsenäisesti samalla avaimella. Lohkojen välillä ei ole ketjutusta eikä alustusvektoria (initialization vector) ole. Tämä on epätavallinen valinta nykystandardeilla, joissa ECB-tilaa yleensä vältetään, mutta yhteensopivuus ei ole paikka alkaa arvailla määrityksiä uudelleen. Excel purkaa paketin salauksen ECB-tilassa, joten tuottajan on salattava se ECB-tilassa tai osapuolet eivät täsmää.
Virhe oli siinä, että paketti salattiin AES-salauksella CBC-tilassa käyttäen kokonaan nollasta koostuvaa alustusvektoria. Tässä on syy siihen, miksi se melkein toimii, ja miksi melkein on huonoin paikka päätyä. CBC-tilassa ensimmäinen selkokielilohko XOR-operaatioidaan alustusvektorin kanssa ennen salausta. Kun alustusvektori on pelkkää nollaa, XOR ei muuta mitään, joten ensimmäinen lohko CBC-tilassa nolla-alustusvektorilla tuottaa täsmälleen saman salatekstin kuin ECB. Toisesta lohkosta eteenpäin CBC syöttää edellisen salatekstilohkon seuraavaan, joten jokainen lohko ensimmäisen jälkeen eroaa ECB-tilasta.
Aseta tämä nyt rakenteen päälle. Paketin leiska asettaa 8-tavuisen little-endian-pituusetuliitteen aivan alkuun, joten ne tiedoston osat, jotka Excel tarkistaa aikaisimmin, sijaitsevat ensimmäisessä tai toisessa lohkossa. Ensimmäinen lohko, joka sattuu täsmäämään, tarkoittaa aikaisimman vahvistuksen menevän läpi, kun taas jokainen myöhempi lohko purkaantuu kohinaksi. Korjaus ei ole hienovaraisuutta, kun tila on nimetty: salaa jokainen 16-tavuinen lohko ECB-tilassa ja lopeta ketjutus. Moottorissa XlsEncryptStdPackage käy puskuroitua puskuria läpi 16-tavuisin askelin ja kutsuu funktiota AESEncryptECB128Block kullekin, mikä on sama primitiivi, jota jo käytettiin todennuslohkoissa. Lähdekoodissa on silmukan kohdalla kommentti, joka ilmoittaa säännön selvästi: CBC nolla-alustusvektorilla vastaa ECB-tilaa vain ensimmäisessä lohkossa, joten loput paketista purkaantuisi 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;
Virhe kaksi: RC4-uudelleenavaus ajautuu epätahtiin
Perinteinen .xls-polku käyttää RC4 CryptoAPI -skeemaa, ja sen sääntö on laadultaan erilainen. [MS-OFFCRYPTO] §2.3.6 määrittelee, että salain avataan uudelleen (re-key) jokaisella 1024 tavun lohkorajalla. Virta on jaettu 1024 tavun lohkoihin, tuore RC4-avain johdetaan lohkonumeroille 0, 1, 2 ja niin edelleen, ja kunkin lohkon sisällä avainvirta kulutetaan jatkuvasti tavusta tavuun. Kahden invariantin on pystyttävä yhdessä: avain on luotava uudelleen jokaisella rajalla, ja avainvirta on kulutettava ilman rakoja lohkon sisällä. RC4 on virtasalain, joten sen avainvirta on yksittäinen järjestetty sekvenssi; n-täs tavu, jonka piirrät, määräytyy sen mukaan, kuinka monta tavua olet piirtänyt ennen sitä. Salauksenpurku on sama XOR-operaatio samaa sekvenssiä vasten, mikä tarkoittaa, että tuottajan ja kuluttajan on piirrettävä täsmälleen samat tavut täsmälleen samoissa kohdissa.
Siinä on koko vaikeus. Virtasalaimella ei ole uudelleensynkronointia. Jos tuhlaat yhdenkin avainvirran tavun, jokainen sen jälkeinen tavu XOR-operaatioidaan väärää avainvirran tavua vasten, eikä virhe koskaan korjaannu itsestään; se etenee lohkon loppuun ja, kun suoritusasema on väärä, jokaiseen lohkoon sen jälkeen. Virhe tässä teki juuri niin. Lohkolaskuri alkoi vartijan (sentinel) arvosta miinus yksi, ja ohitusvahuus oletti laskurin jo vastaavan nykyistä lohkoa. Tuosta vartijasta alkaen se alusti avaimen uudelleen ja ajoi täyden 1024 tavun lohkon avainvirtaa, jota ei olisi koskaan pitänyt kuluttaa, ja prosessissa se ajoi jäljellä olevan määrän negatiiviseksi. Siitä pisteestä lähtien salauksenpurkaja oli kokonaisen lohkon verran epävaiheessa. Todennus, joka tarkistettiin ennen kaikkea tätä, meni silti läpi, joten salasana näytti oikealta, kun taas jokainen tietosolu palautui roskana.
Korjattu logiikka elää luokassa TXLSDecrypterRC4. Sekä Skip- että Decrypt-metodit jakavat yhden silmukan: avain alustetaan uudelleen vain silloin, kun suoritusasema siirtyy uuteen lohkoon, jossa lohkoindeksi on sijainti jaettuna arvolla REKEY_BLOCK_SIZE (1024), ja kulutetaan sitten enintään nykyisen lohkon loppuosa eikä enempää. MakeKey-metodia kutsutaan lohkoindeksillä, ei koskaan vanhentuneella tai vartija-indeksillä, ja asema etenee prosessoitujen tavujen tarkan määrän mukaan, jotta Skip ja Decrypt pysyvät vaihekohdistettuina tuottajan kanssa. Opetus istuu pienimmässä yksikössä: yksittäinen hukattu tavu ei ole pieni virhe virtasalaimessa, se on kaiken alavirran 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;
Yhteensopivuus jäätyneen määrityksen kanssa on tavukohtaista täsmäämistä
Molemmat virheet palautuvat samaan perusperiaatteeseen, ja se on syytä todeta erikseen, koska se muuttaa tapaa, jolla punnitset suunnitteluvaihtoehtoja. Kun tulosteesi kuluttaja on kiinteä ulkoinen ohjelma, jota et voi muuttaa, salausmenetelmä ja uudelleenalustuksen sykli eivät ole toteutuksen yksityiskohtia, joita pääset optimoimaan tai yksinkertaistamaan. Ne ovat osa lankasopimusta. Excel purkaa salauksen ECB-tilassa ja alustaa avaimen uudelleen 1024 tavun rajoilla, miellyttävätpä nämä valinnat sinua tai eivät, ja ainoa tehtäväsi on tuottaa tavuja, jotka purkautuvat alkuperäisiksi tuon tarkan menettelyn mukaisesti. Modernimpi menetelmä, vaarattomalta näyttävä alustusvektori tai laskuri, joka alkaa siitä missä tuntuu luonnolliselta - mikä tahansa näistä on vika heti, kun se poikkeaa siitä, mitä lukija odottaa. Yhteensopivuus jäätynyttä määritystä vastaan ei ole sinne päin. Se on tavukohtaista tai se on rikki.
Tästä syystä todennus on yksinään huono pikatesti (smoke test). Se kertoo avaimen johtamisen toimivan, 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 rungon ollessa lukukelvoton. Todellinen testi purkaa paketin salauksen ja vertaa palautettuja tavuja alkuperäiseen syötteeseen, tai round-trippaa työkirjan salauksen ja salauksenpurun läpi ja lukee solut takaisin. Todennus todistaa salasanan; vain runko todistaa salauksen.
Tuettu tapa lukea ja kirjoittaa suojattuja työkirjoja
Julkinen pinta on pieni. Kirjoittaaksesi salasanalla suojatun modernin työkirjan, täytä tai avaa TXLSXWorkbook ja kutsu SaveAsEncrypted-metodia tiedostonimellä ja salasanalla; se serialisoi työkirjan ja ajaa Standard Encryption -putken, jonka ensimmäinen korjaus korjasi, palauttaen 1 onnistumiselle. Lukeaksesi, kutsu CanReadEncrypted-metodia testataksesi, onko tiedosto salattu Compound File -säiliö, ja haaraudu sitten: OpenEncrypted käsittelee salatun polun ja palaa Open-metodiin tavallisille tiedostoille, ja Open salasanalla on käytettävissä suoraan. Yllä kuvattu menetelmän käsittely ja uudelleenalustussilmukka istuvat näiden kutsujen alla; sinä toimitat salasanan ja tiedostonimen, ja moottori täsmää määrityksen 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 muoto, EncryptionInfo-virta, todennuslohkot ja paketin asettelu käsitellään our walkthrough of AES-protected XLSX output. For the separate question of sheet-level locking and how protection interacts with page setup and printing, see the article on protection, page setup, and printing. Both build on the encryption path described here, which ships as part of the HotXLS spreadsheet component for Delphi and C++Builder alongside the reading, writing, and rendering APIs covered elsewhere on this blog.