Techninis straipsnis

Užšifruoto PDF atvėrimas Delphi su neapdorotu failo raktu

PDF Library for Delphi gali atverti užšifruotą PDF naudojant ne slaptažodį, o neapdorotą failo šifravimo raktą. DAOpenFileWithEncryptionKey priima raktą kaip šešioliktainį tekstą, patikrina jį pagal šifravimo žodyne jau saugomą tikrintuvą ir grąžina tik skaitymo Direct Access rankeną; DAOpenFromStreamWithEncryptionKey tą patį atlieka su iškvietėjo valdomu TStream. Abu metodai atsirado v3.496.0

Scenarijus siauras, bet tikras. Forensikos užduotis pateikia iš atminties atvaizdo atkurtą raktą, tačiau slaptažodžio nėra. Masinio archyvavimo procese dešimt tūkstančių dokumentų failų raktai saugomi depozito duomenų bazėje, nes pradinė DRM sistema slaptažodžių nebeišduoda jau daugelį metų. Pereinant nuo nebenaudojamo teisių valdymo produkto lieka rakto medžiaga ir nieko daugiau. Visais šiais atvejais turimas kredencialas yra raktų išvedimo rezultatas, o ne įvestis, ir nė vienas API slaptažodžio parametras jo nepriims

Kodėl failo šifravimo raktas nėra slaptažodis?

Slaptažodis ir failo šifravimo raktas PDF standarto saugumo tvarkyklėje (ISO 32000-1 §7.6.3) yra skirtingose raktų išvedimo pusėse. Tvarkyklė paima slaptažodį, sumaišo jį su /O, /P, failo ID ir konkrečiai redakcijai skirtu maišu bei gauna failo raktą. Į slaptažodžio vietą padėjus failo raktą, gaunama beprasmybė, maišoma į kitą beprasmybę, todėl reikalingas atskiras entry point, o ne DAOpenFile vėliava

Vieta, į kurią galima įterpti neapdorotą raktą, priklauso nuo redakcijos. 2–4 redakcijos vis dar iš failo rakto, objekto numerio, generacijos numerio ir AESV2 atveju AES druskos išveda atskirą kiekvieno objekto raktą, todėl failo rakto turėjimas neleidžia išvengti objektų lygio išvedimo. 5–7 redakcijos 32 baitų failo raktą tiesiogiai naudoja AES-256, be objekto lygio etapo. Bendra abiem redakcijoms lieka pats failo raktas, todėl tik į jį PDF Library for Delphi priima išoriškai pateiktą raktą. Jei slaptažodį vis dar turite, rinkitės įprastą kelią ir leiskite slaptažodžio pakartotinių bandymų ciklui tvarkyti pirmą neteisingą bandymą, nes neapdoroto rakto entry point sąmoningai atsisako kelių patogumų, kuriuos išlaiko slaptažodžio kelias

Kokią įvestį priima DAOpenFileWithEncryptionKey?

Tik neperrašytą, be tarpų, lyginio ilgio ASCII šešioliktainį tekstą, o iškoduotų baitų skaičius turi tiksliai atitikti šifravimo redakciją. 2–4 redakcijose tikėtinas ilgis imamas iš /Length šifravimo žodyne: bitų kartotinis, patenkantis tarp 5 ir 16 baitų, o nesant /Length numatoma 40 bitų. 5–7 redakcijose ilgis yra lygiai 32 baitai, be derybų. Prefiksas 0x, nelyginis skaitmenų skaičius, daugiau nei 64 šešioliktainiai simboliai, nežinomas bitas Options arba visai nešifruotas dokumentas duoda tą patį rezultatą: rankena 0 ir LastErrorCode, nustatytas į PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID, t. y. 425. Griežtumas čia svarbus. Atlaidi analizė, kuri nukerpa tarpus ir papildo trumpą įvestį nuliais, be vargo pavers nukirptą iškarpinės kopiją kredencialu, o tada žlugs daug mažiau aiškioje vietoje

Var
  Lib: TPDFlib;
  FileHandle, PageRef: Integer;
Begin
  Lib:= TPDFlib.Create;
  Try
    // KeyHex yra 32 šešioliktainiai simboliai AES-128 R4 ir 64 AES-256 R6
    FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
    If FileHandle= 0 Then
      Raise Exception.CreateFmt('raw key refused (error %d)', [Lib.LastErrorCode]);
    Try
      PageRef:= Lib.DAFindPage(FileHandle, 1);
      WriteLn(Lib.DAExtractPageText(FileHandle, PageRef, 0));
    Finally
      Lib.DACloseFile(FileHandle);
    End;
  Finally
    Lib.Free;
  End;
End;

Kiti klaidų kodai išlaikomi atskiri, kad paketinis darbas galėtų atskirti operatoriaus klaidą nuo įrodymų problemos: 411, kai failo nėra, 401, kai jo negalima atverti skaitymui, 409, kai sugadinta kryžminių nuorodų struktūra. Viskas, kas susiję su raktu, tyčia sutraukiama į 425, nes neapdoroto rakto entry point, pranešantis, kuri rakto dalis buvo neteisinga, taptų oracle

Ką iš tikrųjų įrodo tikrinimas?

PDF Library for Delphi įrodo, kad pateiktas raktas priklauso šiam dokumentui, naudodama tikrintuvą, kurį jau turi šifravimo žodynas, o patikra skiriasi pagal redakciją. 2 redakcija perskaičiuoja RC4 šifravimą 32 baitų standartinio užpildo eilutei ir visus 32 baitus palygina su /U. 3 ir 4 redakcijos maišo užpildą su failo ID, vykdo RC4 etapą ir 19 XOR pagrįstų raundų bei palygina pirmus 16 /U baitų. 5–7 redakcijos iššifruoja 16 baitų /Perms eilutę su nuliniu IV ir vienu metu tikrina keturis nepriklausomus dalykus: little-endian leidimų žodį pagal /P, keturis FF baitus 5–8 pozicijose, metaduomenų šifravimo vėliavą kaip T arba F ir adb žymę 10–12 pozicijose

Kai tikrintuvas yra, bet nesutampa, atvėrimas visada atmetamas. Tai verta pasakyti aiškiai, nes nuo šios garantijos priklauso visa funkcija. Taip pat svarbu, ko tikrinimas neįrodo: jis sako, kad raktu galima iššifruoti šį failą, bet ne tai, kad kas nors suteikė jums teisę jį naudoti. Iš /Perms atkurtas leidimų žodis yra rakto įrodymas, o ne leidimas, ir jei norite žinoti, ką dokumentas iš tikrųjų teigia leidžiantis, tai atskira užduotis, kurią atlieka šifravimo ir leidimų audito etapas. Slaptažodžio pusės normalizavimo problemos, pavyzdžiui, SASLprep tvarkymas ne ASCII AES-256 slaptažodžiams, čia paprasčiausiai neatsiranda, nes jokia eilutė nepasiekia maišo

Kada taikoma PDF_RAW_KEY_ALLOW_UNVERIFIED?

PDF_RAW_KEY_ALLOW_UNVERIFIED apima tik vieną situaciją: dokumente nėra tinkamo tikrintuvo, nes /Perms trūksta arba jis nėra 16 baitų, arba /U per trumpas palyginimui. Ji negali panaikinti nesutampančių įrodymų. Sugadinkite vieną šešioliktainį /Perms skaitmenį ir su nustatyta parinktimi pateikite teisingą raktą – PDF Library for Delphi vis tiek grąžins 0 ir 425. Pateikite 32 nulinių baitų raktą prieš nepažeistą tikrintuvą su nustatyta parinktimi – atsakymas bus toks pats. Parinktis sušvelnina įrodymo nebuvimą, bet niekada neignoruoja prieštaravimo. Kadangi atkūrimo atvėrimas ir patikrintas atvėrimas yra skirtingos pažinimo būsenos, jos taip pat pranešamos atskirai, o DAGetEncryptionKeyValidation iš atvertos rankenos grąžina vieną iš trijų konstantų:

  • PDF_RAW_KEY_VALIDATION_VERIFIED (1) reiškia, kad tikrintuvas buvo ir sutapo
  • PDF_RAW_KEY_VALIDATION_UNVERIFIED (2) reiškia, kad raktas priimtas tik todėl, jog tikrintuvo nebuvo galima įvertinti, o iškvietėjas aiškiai paprašė šios politikos
  • PDF_RAW_KEY_VALIDATION_NONE (0) gaunama iš įprastai slaptažodžiu atvertos rankenos
FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex);
If (FileHandle= 0)And
   (Lib.LastErrorCode= PDFLIB_ERROR_RAW_ENCRYPTION_KEY_INVALID) Then
  // šiame faile nėra tikrintuvo? kartokite pagal aiškią atkūrimo politiką
  FileHandle:= Lib.DAOpenFileWithEncryptionKey(CaseFile, KeyHex,
    PDF_RAW_KEY_ALLOW_UNVERIFIED);
If FileHandle<> 0 Then
Begin
  Case Lib.DAGetEncryptionKeyValidation(FileHandle) Of
    PDF_RAW_KEY_VALIDATION_VERIFIED:
      Chain.Note('key verified against the encryption dictionary');
    PDF_RAW_KEY_VALIDATION_UNVERIFIED:
      Chain.Note('no verifier available: extraction is unattested');
  End;
End;

Tik skaitymui pagal konstrukciją ir kam priklauso srautas

Neapdoroto rakto failo entry point visada atveria šaltinį su fmOpenRead or fmShareDenyWrite ir visą Direct Access grandinę pažymi tik skaitymui, todėl DAAppendFile atsisako rašyti vietoje ir grąžina 2, užuot bandęs atlikti prieauginį atnaujinimą. Tai ne politika, nuo kurios rankeną galima įkalbėti atsitraukti; ji nustatoma konstruktoriuje dar prieš analizuojant failą. Dirbant su įrodymais svarbu, kad šaltinio baitai po rankenos uždarymo būtų identiški, ir regresijos rinkinys tai tiksliai tikrina tiek AES-128 4 redakcijos, tiek AES-256 6 redakcijos faile. Srauto entry point rašymo atžvilgiu elgiasi taip pat ir prideda dar vieną taisyklę: DAOpenFromStreamWithEncryptionKey niekada neperima nuosavybės, todėl DACloseFile palieka jūsų TStream gyvą, o atlaisvinti jį turite patys

Source:= TMemoryStream.Create;
Try
  Source.LoadFromFile(ArchivePath);
  Source.Position:= 0;
  FileHandle:= Lib.DAOpenFromStreamWithEncryptionKey(Source, KeyHex);
  If FileHandle<> 0 Then
  Try
    // 2 = tik skaitymo rankena: eksportuokite kitur, niekada nepridėkite prie įrodymų
    Assert(Lib.DAAppendFile(FileHandle)= 2);
    Harvest(Lib, FileHandle);
  Finally
    Lib.DACloseFile(FileHandle);
  End;
Finally
  Source.Free;   // rankena niekada nebuvo šio srauto savininkė
End;

Rakto higiena ir ką eksportuoja DLL

Uždarius grandinę perrašomas failo raktas, slaptažodžių podėlis ir išvesti objektų raktai, o iškoduotas raktas nušluojamas pačiame entry point Finally bloke, nesvarbu, ar atvėrimas pavyko. Už to slypi subtilumas, pareikalavęs tikro derinimo laiko: bet kuri išlikti turinti kopija aiškiai klonuojama su SetLength ir Move, o ne priskiriama. Delphi vieną AnsiString priskyrus kitam abu vardai dalijasi vienu buferiu dėl copy-on-write, todėl nušluosčius iškvietėjo pusę būtų nunulintas raktas, kurį šifravimo tvarkyklė dar naudoja, ir dokumentas būtų iššifruotas į šiukšles be jokio paaiškinamo stack trace. Per DLL ribą eina tik failo entry point, ANSI ir wide formos bei tikrinimo būsenos prieigos metodas; TStream variantas lieka tik Delphi pusėje, nes priklauso nuo Delphi objektų gyvavimo trukmės ir nuorodų semantikos, neturinčios sąžiningo atvaizdavimo plokščioje C ABI. Jei jūsų atkūrimo įrankis yra DLL klientas, planuokite tarpinį failą ir jo ištrynimą pagal tas pačias kontrolės taisykles, kurias taikote raktui

Laikykite šešioliktainį raktą kredencialo medžiaga ir tvarkykite jį taip, kaip tvarkytumėte dokumento slaptažodį, o tikrinimo būseną įrašykite į grandinės, kuri fiksuoja įrodymų kilmę, žurnalą, kad vėliau būtų galima atskirti patikrintą išgavimą nuo nepatvirtinto. Jei vertinate Delphi PDF komponentą forensikai, masiniam archyvavimui ar DRM migracijai, neapdoroto rakto entry point, tik skaitymo garantija ir šifravimo audito paviršius priklauso tai pačiai bibliotekai, o visą funkcijų sąrašą rasite PDF Library for Delphi produkto puslapyje