Techninis straipsnis

SASLprep AES-256 PDF slaptažodžiai Delphi su PDFlibPas

AES-256 šifruotas PDF su ne-ASCII slaptažodžiu atsiveria toje programoje, kuri jį parašė, ir niekur kitur. Priežastis beveik visada — trūkstamas paruošimo žingsnis: ISO 32000-2 §7.6.4.3.3 reikalauja, kad slaptažodis būtų apdorotas su SASLprep stringprep profiliu, prieš jį užkoduojant UTF-8 ir maišant. PDFlibPas, PDF biblioteka Delphi ir C++Builder platformoms, šį paruošimą atlieka viduje Encrypt, EncryptFile ir DecryptFile

Tai — ne neteisingo slaptažodžio istorija ir ne leidimų bitų istorija. Jei jūsų vartotojai renka slaptažodį, kurio niekada neišdavėte, bandymų kartojimo mechanizmas straipsnyje apie šifruoto PDF slaptažodžio pakartotinių bandymų ciklą yra tai, ko jums reikia, o jei bandote išsiaiškinti, ką iš tikrųjų vykdo esamas failas, šifravimo ir leidimų auditas apima tą sritį. Ši istorija siauresnė ir keistesnė: slaptažodis teisingas, vartotojas jį įvedė teisingai, o failas vis tiek atsisako atsiverti kitur

Kodėl ne-ASCII slaptažodis atsiveria vienoje programoje, bet ne kitoje?

Todėl, kad dvi programos maišo skirtingas baitų sekas iš tų pačių klavišų paspaudimų. 6-osios redakcijos rakto kildinimas ISO 32000-2 §7.6.4.3.3 slaptažodį ima kaip UTF-8 baitus, apkarpo iki 127 baitų, prideda druską ir vykdo sustiprintą maišą; rezultatas tikrinamas su /U ir /O įrašais šifravimo žodyne. Nieko toje grandinėje nėra neapibrėžta. Vienas skirtingas baitas bet kurioje įvesties vietoje sukuria visiškai kitą santrauką, patikra nepavyksta, ir skaitytuvas gali pasakyti tik vieną dalyką: neteisingas slaptažodis

Baitai išsiskiria, nes Unikodas siūlo kelis būdus įvesti tai, kas atrodo kaip tas pats slaptažodis. Kinų kalbos slaptažodis gali ateiti kaip sudėtiniai simboliai iš vieno įvesties metodo ir kaip suderinamumo formos iš kito. Vokiškas ar prancūziškas slaptažodis, nukopijuotas iš teksto redaktoriaus, gali nešti NEPERTRAUKIAMĄ TARPĄ (U+00A0) ten, kur vartotojas tiki, jog yra paprastas tarpas, arba MINKŠTĄ BRŪKŠNELĮ (U+00AD), kuris atvaizduojamas kaip niekas. SASLprep egzistuoja tam, kad visa tai suvestų į vieną kanoninę formą prieš bet kam ką nors maišant, kad kiekviena atitinkanti realizacija iš to paties ketinimo kildintų tą patį raktą

Ką SASLprep iš tikrųjų pakeičia slaptažodyje?

RFC 4013 apibrėžia SASLprep kaip stringprep karkaso, aprašyto RFC 3454, profilį, ir tai — keturi eiliniai žingsniai, o ne vienas pertvarkymas. Pirmiausia eina atvaizdavimas: RFC 3454 lentelė C.1.2 (ne-ASCII tarpai) atvaizduojama į U+0020, o lentelė B.1 (simboliai, dažniausiai atvaizduojami į nieką) visai pašalinama. Toliau seka normalizavimas į Unikodo NFKC, tai — žingsnis, sulankstantis suderinamumo simbolius ir kombinuojančias sekas. Tada draudžiamos išvesties patikra atmeta viską, kas yra lentelėse nuo C.2.1 iki C.9. Galiausiai dvikryptė taisyklė iš RFC 3454 6 skirsnio taikoma normalizuotai eilutei

PDFlibPas realizuoja visą profilį PDFlibSASLprep module, kuris atveria vieną įėjimo tašką. PLSASLprepPassword ima žalią slaptažodį, įrašo paruoštą formą į var parametrą ir grąžina False, kai slaptažodis turi būti atmestas. Funkcija sąmoningai visiška laimingu keliu: ASCII slaptažodis grįžta baitas į baitą identiškas, todėl esamiems diegimams niekas nesikeičia

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 nevienareikšmiškumas, kurio lentelės neišsprendžia

Vienas kodo taškas patenka į dvi RFC 3454 lenteles vienu metu, ir šios dvi lentelės nesutaria. NULINIO PLOČIO TARPAS (U+200B) patenka į C.1.2 diapazoną U+2000–U+200B, kur taisyklė sako jį atvaizduoti į U+0020, ir taip pat patenka į B.1 diapazoną U+200B–U+200D, kur taisyklė sako jį pašalinti. Skaitykite atvaizdavimo žingsnį bet kuria tvarka, ir gausite skirtingus baitus iš to paties slaptažodžio: a+U+200B+b paruošiamas kaip a b pagal C.1.2 ir kaip ab pagal B.1. RFC 4013 mini abi lenteles ir nepasako, kuri laimi, todėl tai — tikra nevienareikšmiškumas specifikacijoje, ne skaitymo klaida. PDFlibPas pirmiausia tikrina C.1.2 narystę ir todėl U+200B atvaizduoja kaip tarpą, tai — elgesys, prie kurio nusistovėjo kitos plačiai paplitusios stringprep realizacijos; suderinamumas su jomis yra vienintelis svarbus dalykas čia, nes tikslas — baitų sutapimas su bet kuriuo skaitytuvu, kurį klientas naudoja

Senų failų skaitymas: pirma paruoštas, tada žalias

Pataisymas sukuria savo pačios suderinamumo problemą. Kiekvienas AES-256 failas, parašytas prieš pakeitimą, maišė žalią UTF-8 slaptažodį, todėl skaitytuvo pavertimas griežtai atitinkančiu užrakintų klientus nuo jų pačių archyvų. PDFlibPas tai sprendžia skaitymo pusėje, bandydama du kandidatus iš eilės. TPDFDocument.SetPassword sudaro kandidatų sąrašą, kuris prasideda paruošta forma ir pereina prie žalios formos, ir prideda paruoštą įrašą tik tada, kai dokumentas iš tikrųjų yra AES-256 ir dvi formos skiriasi. ASCII slaptažodžiui formos identiškos, sąraše yra vienas įrašas, o viso mechanizmo kaina — vienas eilučių palyginimas. DecryptFile daro tą patį savo tiesioginio AES-256 perrašymo kelyje, kviesdama PLDirectDecryptFileAES256 pirma su paruoštu slaptažodžiu

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;

Atsarginis kelias turi vieną apsaugą, vertą pasiskolinti. Antrasis bandymas DecryptFile vyksta tik tada, kai paruošta ir žalia formos skiriasi ir pirmasis bandymas nepranešė jokio rimto klaidos kodo. Struktūrinė nesėkmė reiškia, kad įvestis sugadinta arba tai ne ta šifravimo redakcija, kurią jūs manėte esant, o bandymas pakartoti sugadintą failą su kitu slaptažodžiu tiesiog sudegina dar vieną pilną priešiškos įvesties analizę; šios refleksijos loginis pagrindas išdėstytas užraše apie nepatikimų PDF analizę saugiai. Taip pat pastebėkite, kad rašymo pusėje jokio atsarginio kelio nėra, ir ši asimetrija — sąmoninga. Skaitymas toleruoja istoriją, rašymas — ne: kiekvienas naujas AES-256 failas gauna atitinkančius baitus

Kurie slaptažodžiai atmetami visiškai, ir kas yra klaida 604?

SASLprep gali visiškai atmesti slaptažodį, ir kai tai įvyksta, šifravimas turi nepavykti garsiai, o ne tyliai pakeisti kažkuo kitu. Encrypt ir EncryptFile paruošia ir savininko, ir vartotojo slaptažodžius, kai Strength yra 3 arba 4, grąžina 0 atmetimo atveju ir nustato LastErrorCode į PDFLIB_ERROR_PASSWORD_SASLPREP, kuris yra 604. Dvi įvesties šeimos tai sukelia. Draudžiamos išvesties lentelės atmeta valdymo simbolius (C.2.1 ir C.2.2), privataus naudojimo kodo taškus (C.3), ne-simbolius (C.4), pavienius surogatus (C.5), U+FFFD (C.6), ideografinius aprašymo simbolius (C.7) bei rodymo valdymo ir žymėjimo diapazonus (C.8 ir C.9). Atskirai, RFC 3454 6 skirsnio dvikryptė taisyklė atmeta bet kokią eilutę, turinčią RandALCat simbolį iš D.1 lentelės, nebent eilutė ir prasideda, ir baigiasi tokiu simboliu ir apskritai neturi jokių iš kairės į dešinę rašomų raidžių

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;

Ta dvikryptė taisyklė — tai, kas nustebins jūsų palaikymo skyrių. Arabiškas ar hebrajiškas slaptažodis, besibaigiantis vakarietišku skaitmeniu, arba turintis pasiklydusią lotynišką raidę viduryje, pagal specifikaciją atmetamas, net jei įvedimo laukelyje jis atrodo visiškai pagrįstas. Rodykite 604 kaip pranešimą apie slaptažodžio simbolius, ne kaip bendrą šifravimo nesėkmę, kitaip kas nors praleis popietę ieškodamas klaidos savo rakto kildinime

Sąžiningos ribos: NFKC, apytikslis LCat ir vienas Delphi spąstas

Dvi realizacijos dalys — apytikslės, ir abi verta aiškiai įvardyti, o ne palaidoti. NFKC normalizavimą atlieka Windows NormalizeString API, dinamiškai įkeliamas iš Normaliz.dll. Kai ta biblioteka neprieinama, naudojama atvaizduota, bet nenormalizuota eilutė, o tai reiškia, kad atvaizdavimo ir draudimo žingsniai vis tiek vyksta, bet suderinamumo sulankstymas — ne. Praktiškai ši DLL buvo pristatoma su kiekviena Windows versija nuo Vista, todėl degraduotas kelias — tai iki-Vista ir ne-Windows problema, o ne aktuali. Bet slaptažodis, kuris priklauso nuo NFKC sulankstymo, ten sukurtų skirtingus baitus, ir tai — realus, nors ir retas, skirtumas. Dvikryptis patikrinimas — antra aproksimacija: LCat simbolių aptikimas naudoja įprastus raidžių diapazonus vietoj pilnos RFC 3454 D.2 lentelės, ir šios klaidos kryptis kaip tik ir daro ją priimtiną. Praleistas LCat simbolis gali sukelti tik tai, kad dvikryptė taisyklė praeis ten, kur specifikacija būtų atmetusi, bet niekada atvirkščiai, ir tai niekada nepaliečia atvaizdavimo ar normalizavimo žingsnių, todėl priimto slaptažodžio paruošta baitų seka nepakinta. Likutinė rizika todėl — politikos skirtumas, ne baitų skirtumas: egzotiško rašto slaptažodis, kurį griežtesnė realizacija atsisakytų priimti visai. Kiekvienas slaptažodis, kurį priima abi pusės, maišomas identiškai, ir tai — savybė, nuo kurios suderinamumas iš tikrųjų priklauso

Galiausiai, Delphi sintaksės spąstas, kainuojantis valandą, jei niekada su juo nesusidūrėte. Kai funkcija grąžina procedūrinį tipą, jos priskyrimas be skliaustų jos neiškviečia. Kompiliatorius Proc := GetNormalizeProc; skaito kaip GetNormalizeProc paties adreso paėmimą, tada praneša E2009 su nelabai naudinga pastaba, kad iškvietimo konvencijos skiriasi, nes prieigos funkcija naudoja numatytąją konvenciją, o importuotos API tipas — stdcall. Tušti skliaustai — privalomi

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;

Slaptažodžio paruošimas — viena tų detalių, kuri niekada nepasirodo funkcijų sąraše ir nusprendžia, ar šifruotas dokumentas išgyvens susidūrimą su klientu kitoje lokalėje. Encrypt, EncryptFile, DecryptFile ir SetPassword įėjimo taškai, aprašyti čia, yra losLab PDF Developer Library Pascal Edition Delphi ir C++Builder platformoms dalis, kurios produkto puslapyje pateikta pilna šifravimo dokumentacija ir visa klaidos kodų lentelė