Tekninen artikkeli

SASLprep AES-256 PDF-salasanat Delphissä PDFlibPasilla

AES-256-salattu PDF, joka on salattu ei-ASCII-salasanalla, avautuu ohjelmassa, joka sen kirjoitti, eikä missään muualla. Syy on lähes aina puuttuva valmisteluvaihe: ISO 32000-2 §7.6.4.3.3 vaatii, että salasana käsitellään stringprepin SASLprep-profiililla ennen kuin se UTF-8-koodataan ja tiivistetään. PDFlibPas, PDF-kirjasto Delphille ja C++Builderille, suorittaa tämän valmistelun sisäisesti Encrypt-, EncryptFile- ja DecryptFile-funktioissa

Tämä ei ole väärä-salasana-tarina eikä käyttöoikeusbittien tarina. Jos käyttäjäsi kirjoittavat salasanaa, jota et koskaan antanut, artikkelin salattujen PDF-salasanojen uudelleenyrittäminen koneisto on se, mitä haluat, ja jos yrität selvittää, mitä olemassa oleva tiedosto todella pakottaa, salauksen ja käyttöoikeuksien auditointi kattaa sen alueen. Tämä on kapeampi ja oudompi: salasana on oikea, käyttäjä kirjoitti sen oikein, ja tiedosto silti kieltäytyy avautumasta muualla

Miksi ei-ASCII-salasana avautuu yhdessä lukijassa mutta ei toisessa?

Koska kaksi ohjelmaa tiivistävät eri tavusekvenssejä samoista näppäinpainalluksista. Revision 6 avaimen johtaminen ISO 32000-2 §7.6.4.3.3:ssa ottaa salasanan UTF-8-tavuina, typistää 127 tavuun, liittää suolan ja ajaa vahvistetun tiivistefunktion; tulos tarkistetaan salaussanakirjan /U- ja /O-merkintöjä vasten. Mikään tuossa ketjussa ei ole epämääräistä. Yksikin poikkeava tavu missä tahansa kohdassa syötettä tuottaa täysin erilaisen tiivisteen, vahvistus epäonnistuu, ja lukijalla on täsmälleen yksi asia, jonka se voi sanoa: väärä salasana

Tavut eroavat, koska Unicode tarjoaa useita tapoja kirjoittaa se, mikä näyttää samalta salasanalta. Kiinalainen salasana voi saapua esikoostettuina merkkeinä yhdestä syöttömenetelmästä ja yhteensopivuusmuotoina toisesta. Sanankäsittelyohjelmasta kopioitu saksalainen tai ranskalainen salasana voi kantaa katkeamattoman välilyönnin (U+00A0), missä käyttäjä uskoo olevan tavallisen välilyönnin, tai pehmeän tavuviivan (U+00AD), joka piirtyy tyhjänä. SASLprep on olemassa taittaakseen kaikki nämä yhdeksi kanoniseksi muodoksi ennen kuin kukaan tiivistää mitään, jotta jokainen yhdenmukainen toteutus johtaa saman avaimen samasta tarkoituksesta

Mitä SASLprep todella muuttaa salasanassa?

RFC 4013 määrittelee SASLprepin RFC 3454:n stringprep-kehyksen profiiliksi, ja se on neljä järjestettyä vaihetta yhden muunnoksen sijaan. Kuvaus tulee ensin: RFC 3454 taulukko C.1.2 (ei-ASCII-välilyönnit) kuvataan U+0020:ksi, ja taulukko B.1 (merkit, jotka yleisesti kuvataan tyhjäksi) poistetaan kokonaan. Sitä seuraa normalisointi Unicode NFKC:hen, mikä on vaihe, joka taittaa yhteensopivuusmerkit ja yhdistelmäsekvenssit. Sitten kielletyn tulosteen tarkistus hylkää kaiken taulukoissa C.2.1–C.9. Lopuksi RFC 3454:n osan 6 kaksisuuntaisuussääntöä sovelletaan normalisoituun merkkijonoon

PDFlibPas toteuttaa koko profiilin PDFlibSASLprep-yksikössä, joka paljastaa yhden sisäänmenopisteen. PLSASLprepPassword ottaa raa'an salasanan, kirjoittaa valmistellun muodon var-parametriin ja palauttaa False, kun salasana on hylättävä. Funktio on tarkoituksella täydellinen onnistuneella polulla: pelkästä ASCII:sta koostuva salasana palautuu tavu tavulta identtisenä, joten mikään olemassa olevissa käyttöönotoissa ei muutu

uses
  PDFlibSASLprep;

var
  Prepared: WideString;
begin
  // RFC 4013: mapping, then NFKC, then prohibited output, then the bidi rule
  PLSASLprepPassword('I' + WideChar($00AD) + 'X', Prepared);  // -> 'IX'   B.1 deletes SOFT HYPHEN
  PLSASLprepPassword('a' + WideChar($00A0) + 'b', Prepared);  // -> 'a b'  C.1.2 maps NBSP to U+0020
  PLSASLprepPassword(WideString(WideChar($00AA)), Prepared);  // -> 'a'    NFKC folds ORDINAL INDICATOR
  PLSASLprepPassword(WideString(WideChar($2168)), Prepared);  // -> 'IX'   NFKC folds ROMAN NUMERAL NINE
  PLSASLprepPassword('user', Prepared);                       // -> 'user' ASCII is never touched
end;

U+200B-epäselvyys, jota taulukot eivät ratkaise

Yksi koodipiste päätyy kahteen RFC 3454 -taulukkoon samaan aikaan, ja nämä kaksi taulukkoa eroavat toisistaan. ZERO WIDTH SPACE (U+200B) osuu C.1.2-alueelle U+2000–U+200B, jossa sääntö sanoo kuvata sen U+0020:ksi, ja se osuu myös B.1-alueelle U+200B–U+200D, jossa sääntö sanoo poistaa sen. Lue kuvausvaihe kummassa tahansa järjestyksessä, ja saat eri tavuja ulos samasta salasanasta: a+U+200B+b valmistuu muotoon a b C.1.2:n mukaan ja muotoon ab B.1:n mukaan. RFC 4013 nimeää molemmat taulukot eikä sano, kumpi voittaa, joten tämä on aito epäselvyys spesifikaatiossa eikä lukuvirhe. PDFlibPas testaa C.1.2-jäsenyyden ensin ja kuvaa siksi U+200B:n välilyönniksi, mikä on käyttäytyminen, johon muut laajalti käytössä olevat stringprep-toteutukset ovat asettuneet; niiden vastaaminen on ainoa asia, jolla tässä on merkitystä, koska tavoitteena on tavutason yhteensopivuus minkä tahansa lukijan kanssa, jota asiakas sattuu käyttämään

Vanhojen tiedostojen lukeminen: ensin valmisteltu, sitten raaka

Korjaus luo oman yhteensopivuusongelmansa. Jokainen ennen muutosta kirjoitettu AES-256-tiedosto tiivisti raa'an UTF-8-salasanan, joten lukijan tekeminen tiukasti yhdenmukaiseksi lukitsisi asiakkaat pois omista arkistoistaan. PDFlibPas ratkaisee tämän lukupuolella kokeilemalla kahta ehdokasta järjestyksessä. TPDFDocument.SetPassword rakentaa ehdokaslistan, joka alkaa valmistellusta muodosta ja palaa raakaan muotoon, ja se lisää valmistellun merkinnän vain, kun asiakirja on todella AES-256 ja kaksi muotoa eroavat toisistaan. ASCII-salasanalle muodot ovat identtiset, lista pitää sisällään yhden merkinnän, ja koko mekanismin kustannus on yksi merkkijonovertailu. DecryptFile tekee saman asian omalla suoralla AES-256-uudelleenkirjoituspolullaan, kutsuen PLDirectDecryptFileAES256:ta valmistellulla salasanalla ensin

var
  Lib: TPDFlib;
  Bytes: AnsiString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.DrawText(100, 100, 'saslprep roundtrip');
    // Strength 3 and 4 are the two AES-256 values; both are prepared before hashing
    Lib.Encrypt('ow' + WideChar($00AD) + 'ner', 'pa' + WideChar($00AD) + 'ss', 4,
      Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1));
    Bytes := Lib.SaveToString;
  finally
    Lib.Free;
  end;

  Lib := TPDFlib.Create;
  try
    // 'pass' is what SASLprep produced and what any conforming reader computes,
    // so the plain ASCII form opens a file created with the soft-hyphen form
    if Lib.LoadFromString(Bytes, 'pass') = 1 then
      Caption := IntToStr(Lib.PageCount);
  finally
    Lib.Free;
  end;
end;

Vaihtoehtoinen polku kantaa yhden vartijan, joka kannattaa kopioida. Toinen yritys DecryptFile:ssa ajetaan vain, kun valmisteltu ja raaka muoto eroavat ja ensimmäinen yritys ei raportoinut kovaa virhekoodia. Rakenteellinen epäonnistuminen tarkoittaa, että syöte on vioittunut tai se ei ole se salausrevisio, jota oletit, ja rikkinäisen tiedoston uudelleenyrittäminen toisella salasanalla vain polttaa toisen täyden jäsennyskierroksen vihamielisen syötteen yli; tämän refleksin taustalla oleva perustelu on esitetty artikkelissa huomio epäluotettavien PDF-tiedostojen turvallisesta jäsentämisestä. Huomaa myös, ettei kirjoituspuolella ole vaihtoehtoista polkua, ja tuo epäsymmetria on tarkoituksellinen. Lukeminen sietää historiaa, kirjoittaminen ei: jokainen uusi AES-256-tiedosto saa yhdenmukaiset tavut

Mitkä salasanat hylätään suoralta kädeltä, ja mikä on virhe 604?

SASLprep voi hylätä salasanan kokonaan, ja kun se tekee niin, salauksen on epäonnistuttava äänekkäästi eikä hiljaa korvattava jotain muuta. Encrypt ja EncryptFile valmistelevat sekä omistajan että käyttäjän salasanat aina, kun Strength on 3 tai 4, palauttavat 0:n hylätessä ja asettavat LastErrorCode:n arvoksi PDFLIB_ERROR_PASSWORD_SASLPREP, joka on 604. Kaksi syötteiden perhettä laukaisee sen. Kielletyn tulosteen taulukot hylkäävät ohjausmerkit (C.2.1 ja C.2.2), yksityiskäyttöön varatut koodipisteet (C.3), ei-merkit (C.4), yksinäiset sijaismerkit (C.5), U+FFFD:n (C.6), ideografiset kuvausmerkit (C.7) sekä näytön ohjaus- ja tagausalueet (C.8 ja C.9). Erikseen RFC 3454:n osan 6 kaksisuuntaisuussääntö hylkää minkä tahansa merkkijonon, joka sisältää RandALCat-merkin taulukosta D.1, ellei merkkijono sekä ala että pääty sellaiseen eikä sisällä lainkaan vasemmalta oikealle -kirjaimia

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    // U+0007 is a C.2.1 control character, so preparation refuses the password
    if Lib.Encrypt('owner', 'bad' + WideChar($0007), 4,
         Lib.EncodePermissions(1, 0, 0, 0, 0, 0, 0, 1)) = 0 then
    begin
      if Lib.LastErrorCode = PDFLIB_ERROR_PASSWORD_SASLPREP then  // 604
        ShowMessage('The password contains characters that PDF encryption does not permit.');
    end;
  finally
    Lib.Free;
  end;
end;

Tuo kaksisuuntaisuussääntö on se, joka yllättää tukipisteesi. Arabialainen tai heprealainen salasana, joka päättyy länsimaiseen numeroon, tai sellainen, jossa on satunnainen latinalainen kirjain keskellä, hylätään spesifikaation mukaan, vaikka se näyttää täysin järkevältä syöttökentässä. Näytä 604 viestinä salasanan merkeistä, ei yleisenä salausvirheenä, tai joku käyttää iltapäivän etsien vikaa avaimenjohtamisestasi

Rehelliset rajat: NFKC, likimääräinen LCat ja yksi Delphi-ansa

Kaksi osaa toteutuksesta ovat likiarvoja, ja molemmat ansaitsevat tulla sanotuiksi suoraan sen sijaan, että ne haudattaisiin. NFKC-normalisointi suoritetaan Windowsin NormalizeString-API:lla, joka ladataan dynaamisesti Normaliz.dll:stä. Kun tuo kirjasto ei ole saatavilla, kuvattua merkkijonoa käytetään normalisoimattomana, mikä tarkoittaa, että kuvaus- ja kieltovaiheet silti ajetaan, mutta yhteensopivuustaitto ei. Käytännössä DLL on toimitettu jokaisen Windows-julkaisun mukana Vistasta lähtien, joten heikentynyt polku on Vistaa edeltävä ja ei-Windows-huoli eikä elävä sellainen, mutta salasana, joka nojaa NFKC-taittoon, tuottaisi siellä eri tavuja, ja se on todellinen, joskin harvinainen, poikkeama. Kaksisuuntaisuustarkistus on toinen likiarvo: LCat-merkkien tunnistus käyttää yleisiä kirjainalueita täyden RFC 3454 -taulukon D.2 sijaan, ja sen virheen suunta on se, mikä tekee siitä hyväksyttävän. Ohitettu LCat-merkki voi vain saada kaksisuuntaisuussäännön läpäisemään tapauksen, jonka spesifikaatio olisi hylännyt, ei koskaan päinvastoin, eikä se koskaan kosketa kuvaus- tai normalisointivaiheita, joten hyväksytyn salasanan valmisteltu tavusekvenssi pysyy muuttumattomana. Jäljelle jäävä riski on siis käytäntöpoikkeama eikä tavupoikkeama: eksoottisen kirjoitusjärjestelmän salasana, jonka tiukempi toteutus kieltäytyisi hyväksymästä lainkaan. Jokainen salasana, jonka molemmat puolet hyväksyvät, tiivistyy identtisesti, mikä on ominaisuus, josta yhteentoimivuus todella riippuu

Lopuksi Delphi-syntaksiansa, joka maksaa tunnin, jos et ole törmännyt siihen aiemmin. Kun funktio palauttaa proseduraalisen tyypin, sen sijoittaminen ilman sulkeita ei kutsu sitä. Kääntäjä lukee Proc := GetNormalizeProc; ottavan GetNormalizeProc:n itsensä osoitteen, ja raportoi sitten E2009:n hyödyttömällä valituksella, että kutsukäytännöt eroavat, koska accessor käyttää oletuskäytäntöä, kun tuotu API-tyyppi on stdcall. Tyhjät sulkeet ovat pakolliset

type
  TNormalizeString = function(NormForm: Integer; SrcString: PWideChar; SrcLength: Integer;
    DstString: PWideChar; DstLength: Integer): Integer; stdcall;

function GetNormalizeProc: TNormalizeString;   // loads Normaliz.dll on first use
...
var
  Proc: TNormalizeString;
begin
  // Proc := GetNormalizeProc;   // E2009: reads as @GetNormalizeProc, conventions differ
  Proc := GetNormalizeProc();    // correct: calls the accessor and assigns its result
  if not Assigned(Proc) then
    Exit;                        // no NFKC available, mapped string is used as-is
end;

Salasanan valmistelu on yksi niistä yksityiskohdista, jotka eivät koskaan näy ominaisuuslistassa mutta ratkaisevat, selviääkö salattu asiakirja kosketuksesta asiakkaan kanssa toisessa lokaalissa. Tässä kuvatut Encrypt-, EncryptFile-, DecryptFile- ja SetPassword-sisäänmenopisteet ovat osa losLab PDF Developer Library Pascal Editionia Delphille ja C++Builderille, jonka tuotesivu kantaa täyden salausviitteen ja täydellisen virhekoodataulukon