Teknisk artikkel

PDF-krypteringsfiltre i Delphi: StmF, StrF og EFF

HotPDF Delphi PDF-komponenten implementerer ISO 32000-1 §7.6.5-modellen for crypt-filtre som tre uavhengige policyer i stedet for én bryter: ConfigureCryptFilterDefaults tildeler strengfilteret /StrF, strømfilteret /StmF og embedded-file-filteret /EFF separat, SetStreamCryptFilter overstyrer én enkelt strøm, og GetLoadedCryptFilterInfo rapporterer det en innkommende fil deklarerer. De fleste interoperabilitetsfeil i krypterte PDF-er bor i mellomrommene mellom disse tre

Her er feilen som sender folk ned på dette laget. Et team leverer et dokument der sideinnholdet må være lesbart for et verktøy nedstrøms, men det vedlagte innholdet ikke skal være det, så de setter /EFF /StdCF og lar /StmF /Identity stå. Acrobat åpner det fint. En samsvarende tredjartsleser gir vedlegget tilbake som kryptert søppel, fordi /EFF er en policy på produsentsiden om hvilket filter som gjelder for embedded files, mens en generell leser fortsatt løser en umerket strøm gjennom /StmF. Løsningen er ikke en annen /EFF-verdi. Løsningen er et eksplisitt /Crypt-filter på selve embedded-file-strømmen

Hva styrer crypt-filterlaget egentlig?

Crypt-filtre ligger mellom krypteringsalgoritmen og objektgrafen, og avgjør hvilke objekter algoritmen berører, ikke hvordan den virker. /CF-ordboken inne i krypteringsordboken mapper navn til filterdefinisjoner, der hver har en /CFM-metode, en valgfri /Length og en /AuthEvent. De tre toppnivåoppføringene /StrF, /StmF og /EFF velger deretter hvilket navngitt filter som gjelder for strenger, strømmer uten et eksplisitt filter og embedded files. HotPDF begrenser med vilje hva de innebygde håndtererne vil skrive. ConfigureCryptFilterDefaults aksepterer bare de reserverte navnene for den aktive håndtereren: Standard security handler skriver /StdCF eller /Identity, public-key handler skriver /DefaultCryptFilter eller /Identity, og alt annet utløser EArgumentException på kallstedet. Filtre som eksterne produsenter har skrevet under andre navn, bevares fortsatt i stiene for lasting, inspeksjon og kompatibilitetsomskriving, så HotPDF er konservativ som writer og ettergivende som reader. To ytterligere vakter gjelder: Kallet utløser EInvalidOpException når dokumentserialiseringen har begynt, og på nytt hvis dokumentet er i en inkrementell oppdatering, fordi krypteringspolicy ikke kan endres mellom revisjoner av den samme filen

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.AutoLaunch := False;
    Pdf.FileName := 'wrapper.pdf';
    Pdf.OwnerPassword := 'owner-secret';
    Pdf.UserPassword := 'open-secret';
    Pdf.CryptKeyLength := aes128;
    // strenger kryptert, sidestrømmer i klartekst, vedlegg kryptert
    Pdf.ConfigureCryptFilterDefaults('StdCF', 'Identity', 'StdCF');
    Pdf.ActivateProtection := True;
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [], 12);
    Pdf.CurrentPage.TextOut(50, 50, 0, 'Visible stream operators');
    Pdf.AddDocumentAttachment('payload.bin', 'Encrypted payload');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Én begrensning bør sies med én gang, fordi den kontrolleres sent og overrasker folk. Navngitte crypt-filtre i HotPDF krever aes128-, aes256- eller aesgcm-dokumentkryptering. Konfigurer en filterpolicy oppå RC4 k40 eller k128, så utløser valideringspasset som kjører når kryptering aktiveres, en feil i stedet for stille å oppgradere nøkkeltypen. Det er samme designholdning som resten av AES-256 PDF-krypteringsstien i Delphi: Avvis den tvetydige konfigurasjonen i stedet for å gjette hva caller-en mente

Hvorfor betyr /Length-oppføringen to forskjellige ting?

Fordi spesifikasjonen definerer den i to forskjellige enheter avhengig av security handler, og HotPDF må respektere begge. I en crypt-filter-ordbok der /CFM er /V2, uttrykkes /Length-oppføringen i byte under Standard security handler og i bit under public-key handler. /Length i krypteringsordboken, som ligger ved siden av /V (ISO 32000-1 §7.6.2), er alltid i bit. Les en filterordbok med /Length 16, og du har en 128-biters nøkkel i en Standard-handler-fil og en avvist fil i en public-key-fil. HotPDF normaliserer dette når den fanger den innlastede konfigurasjonen. Den multipliserer en /V2-filter-/Length med åtte bare når filen ikke er public-key-kryptert, faller tilbake til dokumentnivåets /Length når filteret utelater sitt eget, og lagrer resultatet i THPDFCryptFilterInfo.KeyLengthBits. AESV2 er låst til 128 bit og AESV3 og AESV4 til 256, siden disse metodene ikke har forhandlingsbar nøkkelstørrelse. Det strenge kommer etterpå: Bare 40-bit og 128-bit /V2 godtas. Et filter som løser seg til en annen lengde, rapporteres som utilgjengelig, og operasjonen feiler i stedet for å rundes til 128 med teorien om at de fleste produsenter uansett mente 128. Å normalisere nøkkellengden stille er slik du leverer en fil som dekrypterer på din maskin og ingen andre steder

var
  Reader: THotPDF;
  Info: THPDFCryptFilterInfo;
  I: Integer;
begin
  Reader := THotPDF.Create(nil);
  try
    Reader.AutoLaunch := False;
    if Reader.LoadFromFile('incoming.pdf', 'open-secret') <> 1 then
      Exit;
    // /StrF og /StmF er som standard Identity; /EFF er som standard /StmF
    WriteLn(Reader.LoadedStringCryptFilterName);        // StdCF
    WriteLn(Reader.LoadedStreamCryptFilterName);        // Identity
    WriteLn(Reader.LoadedEmbeddedFileCryptFilterName);  // StdCF
    for I := 0 to Reader.GetLoadedCryptFilterCount - 1 do
      if Reader.GetLoadedCryptFilterInfo(I, Info) then
        if (Info.Method = hcfmV2) and
           not (Info.KeyLengthBits in [40, 128]) then
          raise Exception.CreateFmt(
            'crypt filter /%s: unsupported V2 key length %d',
            [String(Info.Name), Info.KeyLengthBits]);
  finally
    Reader.Free;
  end;
end;

Hva garanterer /CFM /None, og hvordan skiller /Identity seg ut?

De ender med samme resultat gjennom forskjellige veier, og hvis du blander dem sammen, ødelegger du oppslagene. Et navngitt filter hvis /CFM er /None, og et navngitt filter som utelater /CFM helt, betyr begge at dette filteret ikke utfører kryptering eller dekryptering — HotPDF mapper den manglende oppføringen til None før oppløsningen, så begge ender på hcfmNone med en registrert nøkkellengde på null. /Identity er annerledes av natur: Det er det reserverte navnet som hopper over /CF-oppslaget i sin helhet, så et dokument kan referere til /Identity uten å definere det noe sted i /CF. PDF-navn skiller mellom store og små bokstaver, noe som gjør én videre implementasjonsdetalj ufravikelig: Ingen crypt-filter-oppslag kan være ufølsomme for store og små bokstaver. HotPDF løser navn i /CF-underordboken, filterets /Length-oppføring og kontrollen av strøm-/Type gjennom oppslag som skiller mellom store og små bokstaver. En fil som definerer /stdcf mens /StmF peker på /StdCF, er feilformatert, og å behandle de to som samme nøkkel ville gjort en oppdagbar forfatterfeil om til en feil nøkkel som stille brukes på hver strøm i dokumentet

Få /EFF til å sitte på embedded-file-strømmer

Når /EFF avviker fra /StmF, trenger embedded-file-strømmen en eksplisitt innledende /Crypt-oppføring i /Filter og en samsvarende /DecodeParms-ordbok som har /Name på samme indeks i matrisen. HotPDF regner dette ut per strøm ved lagring: Den oppdager /Type /EmbeddedFile, arver filteret for embedded files som er konfigurert og skriver den eksplisitte /Crypt-markøren bare når det arvede navnet avviker fra den effektive strømstandarden. Når /EFF og /StmF er enige, skrives ingen markør, fordi en reader uansett ville løst det samme filteret. Matriseindeksen betyr da like mye som navnet. Når HotPDF leser en strøm tilbake, skanner den /Filter etter /Crypt-oppføringen, registrerer indeksen og slår deretter opp på den samme indeksen i /DecodeParms-matrisen for å finne /Name. En /Crypt på indeks 0 som kobles med parametere på indeks 1, løser seg til /Identity, ikke til filteret ditt. Det er også grunnen til at writer-en fyller parameter-matrisen med null når strømmen tidligere hadde en /Filter, men ingen /DecodeParms: Posisjonene må fortsatt være på linje

Det finnes en skarpere felle under dette. Hvis den eksisterende /Filter- eller /DecodeParms-verdien er et indirekte objekt — vanlig i filer fra generatorer som deler én filtermatrise mellom mange strømmer — ville innsetting av /Crypt på stedet mutert en delt filtergraf og ødelagt alle andre strømmer som peker på den. HotPDF løser det indirekte objektet og kloner det først til et strømprivat direkte objekt, og tømmer objekt- og generasjonsnumrene slik at den opprinnelige indirekte roten aldri bygges inn i den nye matrisen. For en strøm som allerede brukte ASCIIHexDecode, blir det serialiserte resultatet /Filter [ /Crypt /ASCIIHexDecode ] med /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Den samme posisjonsdisiplinen styrer alle andre filterkjeder, inkludert dem du går gjennom når du henter bilder fra en innlastet PDF gjennom dekodefiltrene deres

// Editor har allerede et innlastet dokument, og ContentStream er en
// THPDFStreamObject der /Filter er et indirekte /ASCIIHexDecode-navn
Editor.OwnerPassword := 'owner-secret';
Editor.UserPassword := 'open-secret';
Editor.CryptKeyLength := aes128;
Editor.ConfigureCryptFilterDefaults('StdCF', 'Identity');
Editor.SetStreamCryptFilter(ContentStream, 'StdCF');
Editor.ActivateProtection := True;
Editor.SaveLoadedDocument('out.pdf');

// Et tomt navn fjerner overstyringen og stripper det foreldede /Crypt-
// innslaget sammen med dekodeparameterne ved neste lagring
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');

Arver objektstrømmer dokumentets /Encrypt-policy?

Nei, og å anta det er en pålitelig måte å produsere søppel på. En objektstrøm må følge den faktiske /StmF-policyen eller sin egen eksplisitte /Crypt-markør: Selve tilstedeværelsen av en /Encrypt-ordbok gjør ikke enhver /ObjStm-beholder til ciphertext. Et dokument med /StmF /Identity har plaintext-objektstrømmer selv om strengene er fullstendig kryptert, og en dekoder som dekrypterer dem likevel, mater inflate-fasen med input som aldri var deflate-output

Konsekvensen for medlemsobjekter er delen det er verdt å lese to ganger. I henhold til ISO 32000-1 §7.5.7 er strenger inne i en kryptert objektstrøm allerede i klartekst når selve beholderen er dekryptert, så å dekryptere dem på nytt ville vært dobbel dekryptering. HotPDF vokter dette ved å spørre om beholderen til hvert type-2-objekt var kryptert, hoppe over objektet når den var det og telle hoppene i XRefProbeDecryptObjStmSkips som direkte bevis på at vakten slo inn. Når beholderen var i klartekst, var medlemstrengene aldri dekket av noe, så HotPDF materialiserer disse medlemmene og bruker /StrF på hver enkelt — nøkkelstyrt, slik implementasjonen faktisk gjør det, av medlemsobjektets nummer og generasjon, ikke av nummeret til den omsluttende /ObjStm-objektet. Snu dette på en fil med blandet policy, og hver streng i hvert komprimerte objekt dekodes til støy. Reglene på containernivå er dekket videre i notatene om PDF-objektstrømmer og inkrementelle oppdateringer

Hvor nekter HotPDF å gjette?

Crypt-filter-semantikk finnes ikke under /V 4, så HotPDF avviser enhver overstyring per strøm på en slik fil med en eksplisitt feil i stedet for å skrive en /Crypt-markør som ingen samsvarende reader ville respektert. Det samme gjelder ved lesing: En krypteringsordbok med /V under 4 tømmer alle tre innlastede filternavnene, fordi det ikke finnes noe der å rapportere. Tre ytterligere grenser håndheves med hensikt:

  • Et filter per strøm som ikke er Identity på et public-key-kryptert dokument, avvises, fordi en strømspesifikk policy under public-key-handleren trenger en strømspesifikk mottakerkonvolutt som HotPDF ennå ikke skriver
  • Public-key-krypterte embedded files der /EFF avviker fra den effektive /StmF, avvises av samme grunn, i stedet for å skrives i en form som ingen kan dekryptere
  • Den direkte AES-256-filens hurtigsti gjelder bare når strenger, strømmer og embedded files alle løser seg til samme crypt-filtermetode, og ingen objekter i filen har en eksplisitt /Crypt; en blandet policy eller plaintext-metadata tvinger frem en fallback til hele objektgraf-stien

Ingen av disse er ytelsesbeslutninger. De markerer stedene der en feil gjetning gir en PDF som åpnes i én viewer, feiler i en annen og ikke gir utvikleren noe signal før en kunde rapporterer det. En avvisning ved ConfigureCryptFilterDefaults eller ved lagring koster ett unntak; en stille feilnøkkel på et embedded file koster en supportsyklus. Hvis du bygger Delphi- eller C++Builder-programvare som produserer eller konsumerer kryptert PDF — selektivt klartekstinnhold på sider med krypterte vedlegg, krypterte payload-wrappere i PDF 2.0 eller interoperabilitet med filer der du ikke valgte crypt-filter-policyene — leveres crypt-filter-API-et som er beskrevet her, i den aktuelle HotPDF Delphi PDF-komponenten, sammen med krypterings-, objektstrøm- og inkrementell-oppdateringsstiene den bygger på