Teknisk artikel

Du kan inte tyst uppdatera en krypterad PDF i Delphi

Ta en fakturaPDF som redan bär AES-256-kryptering och be PDFium-komponenten för Delphi och C++Builder (PDFiumPas) stämpla den PDF/A för arkivering, eller signera den med PAdES, genom en inkrementell uppdatering snarare än en fullständig omskrivning. Biblioteket kommer inte dit genom att uppdatera de krypterade byten direkt: dess sex konformitetsmarkör-injektorer upptäcker en befintlig /Encrypt-post och skickar källan igenom byte-för-byte oförändrad, och dess PAdES-signerare kastar ett undantag snarare än att avge en signatur ingen validerare kommer att acceptera

Det är en annan fråga än att granska en PDF man inte skapade för dold risk, vilket är en egen skrivskyddad övning. Den här artikeln handlar om skrivsidan av samma förtroendegräns: vad din egen kod tillåts göra mot en fil vars byte redan är låsta bakom någon annans lösenord, i det ögonblick den koden försöker lägga till något i den i efterhand

Vad kräver ISO 32000-1 när du uppdaterar en krypterad PDF?

ISO 32000-1 §7.5.6 kräver att en inkrementell uppdaterings efterskrift upprepar varje post från den tidigare efterskriften utom /Prev, och Tabell 15 listar /Encrypt bland de poster en efterskrift kan bära. Utelämna den från den nya efterskriften och en konform läsare har ingen anledning att tvivla på utelämnandet: den nyaste efterskriften är auktoritativ, så en läsare som inte hittar någon /Encrypt där bestämmer att hela filen är okrypterad och försöker tolka den äldre, fortfarande krypterade kroppen som vanliga byte. Behåll /Encrypt i den nya efterskriften men skriv uppdateringens egna objekt som klartext, och felet flyttar sig bara ett steg senare: läsaren upptäcker korrekt kryptering, kör varje objekt den rör genom filens chiffer, inklusive de nya som aldrig var krypterade från början, och får brus tillbaka för innehåll som var perfekt läsligt innan dekrypteringen rörde det. Endera misstaget producerar en fil som ser ut som en normal, välformad inkrementell uppdatering på bytenivå, ända fram tills en konform läsare öppnar den

Sex markörinjektorer, en v2.14.2-krypteringsgrind

PDFiumPas levererar sex bytenivåmarkörinjektorer, en för varje ISO PDF-delmängd den kan märka: PDF/A (ISO 19005), PDF/X (ISO 15930), PDF/UA (ISO 14289-1), PDF/E-1 (ISO 24517-1), PDF/R-1 (ISO 23504-1), och PDF/VT-1 (ISO 16612-2). Var och en tar byten PDFiums eget FPDF_SaveAsCopy redan skrivit och lägger en andra, mindre inkrementell uppdatering ovanpå dem: en ny XMP-metadataström, en katalogordboksredigering som pekar på den, och för de tryckorienterade delmängderna en OutputIntent och ICC-profil. Från och med v2.14.2 läser varje en av InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers, och InjectPdfVTMarkers källans efterskrift först, och om den rapporterar en befintlig /Encrypt-post kopierar den källan igenom till målströmmen omodifierad och returnerar omedelbart. Inget XMP, ingen OutputIntent, ingen katalogredigering — anroparen får originalfilen tillbaka, byte för byte

var
  Src, Dst: TFileStream;
  Opts: TPdfXSaveOptions;
begin
  Src := TFileStream.Create('signed-encrypted-proof.pdf', fmOpenRead);
  Dst := TFileStream.Create('pdfx-attempt.pdf', fmCreate);
  try
    Opts.Conformance := pxc4;
    InjectPdfXMarkers(Src, Dst, Opts);
    // pdfx-attempt.pdf is byte-identical to the source: still encrypted,
    // no /GTS_PDFXVersion, no OutputIntent. Nothing was written, and
    // nothing was corrupted either
  finally
    Dst.Free;
    Src.Free;
  end;
end;

Tillåtet att vara krypterad är inte samma sak som säkert att injicera i

PDF/E-1 och PDF/R-1 tillåter båda uttryckligen sitt värddokument att vara krypterat på specifikationsnivå, vilket läser som ett undantag tills man tittar på vad som faktiskt måste hända på disk. ISO 24517-1 §6.3 tillåter kryptering för PDF/E-1, och ISO 23504-1 §6.2.3 tillåter det för PDF/R-1 förutsatt att huvudet deklarerar %PDF-2.0. Ingendera klausulen säger något om huruvida en bytenivå-efterprocessor säkert kan lägga till ett klartextobjekt i den krypterade behållaren, och det kan den inte, av samma §7.5.6-skäl som gäller för varje annan delmängd. PDFiumPas egna konformitetsvaliderare för de här två profilerna, ValidatePdfECompliance och ValidatePdfRCompliance, registrerar förekomsten av /Encrypt medvetet utan att flagga det som en defekt, vilket är korrekt för en skrivskyddad validerare som aldrig skriver en byte. Det är också ett mönster lätt att skumma förbi och anta att syskoninjektorn inte behöver något separat skydd, när injektorn är den ena funktionen i paret som faktiskt måste vägra

Dekrypterar SaveAsPdfX ditt dokument tyst?

Ja, närhelst du går genom de publika bekvämlighetsmetoderna istället för att anropa en injektor direkt. Var och en av TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR, och SaveAsPdfVT rendrerar det aktuella dokumentet till en temporär ström med SaveAs(Tmp, saRemoveSecurity) innan den ger de byten till sin matchande injektor. saRemoveSecurity mappas till PDFiums egen FPDF_REMOVE_SECURITY-flagga, så den temporära kopian injektorn tar emot var aldrig krypterad från början, och injektorns /Encrypt-skydd har aldrig anledning att utlösas. Utdatan bär dina PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1-, eller PDF/VT-1-markörer, men den är inte längre skyddad av vad för lösenord som än öppnade källan

Den avvägningen är osynlig tills någon nedströms öppnar den "skyddade" arkivkopian utan lösenord och märker att det bara fungerar. Fixen är inte ett annat metodanrop; PDFiumPas har ingen saAddSecurity-motsvarighet att para med saRemoveSecurity, eftersom den underliggande PDFium-motorn aldrig byggdes för att skriva ny kryptering, bara för att ta bort den. Om båda egenskaperna spelar roll för en fil måste kryptering vara ett separat steg du äger, tillämpat efter konformitetsmarkörerna, inte infogat i samma SaveAsPdfA-anrop

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';           // needed to open the source at all
    Pdf.FileName := 'signed-encrypted-invoice.pdf';
    Pdf.LoadDocument;
    Pdf.SaveAsPdfA('invoice-pdfa.pdf', pac2b);
    // invoice-pdfa.pdf now declares PDF/A-2b, but SaveAs(saRemoveSecurity)
    // ran first inside SaveAsPdfA: the output opens without a password
  finally
    Pdf.Free;
  end;
end;

Vad händer när du signerar en krypterad PDF med PAdES?

PDFiumPas vägrar rakt av, snarare än att tyst tappa begäran på det sätt en markörinjektor gör. TPdf.SignPades och SignPadesToStream går båda genom en intern SignPadesBytes, och det första den gör efter att ha tolkat källans efterskrift är att kontrollera efter /Encrypt. Om posten finns kastar den EPadesCrypto med meddelandet "SignPadesBytes: the source document is encrypted; remove encryption before signing" istället för att fortsätta vidare. InjectPadesDssMarkers, funktionen som bäddar in certifikat, OCSP-svar, och CRL:er för långsiktig validering, tillämpar den identiska kontrollen av den identiska anledningen, med sitt eget meddelande: "InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material"

Resonemanget här är strängare än markörinjektorernas genomsläpp, och medvetet så. Ett tyst genomsläpp är säkert för en PDF/A-stämpel eftersom att hoppa över det lämnar dig med samma giltiga PDF du började med, bara omärkt. Signering kan inte misslyckas lika tyst: en signatur som tyst aldrig lades till ser, för all anropande kod som bara kontrollerar ett booleskt resultat, exakt ut som en signatur som lades till framgångsrikt. EPadesCrypto härstammar från den vanliga Exception-klassen, så att fånga den är normal undantagshantering, inte en speciell kontrollflödeskonvention du måste lära dig

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.Password := 'open-secret';
    Pdf.FileName := 'encrypted-contract.pdf';
    Pdf.LoadDocument;
    try
      Pdf.SignPades('encrypted-contract-signed.pdf', 'A1B2C3D4E5F6...');
    except
      on E: EPadesCrypto do
        // E.Message: 'SignPadesBytes: the source document is encrypted;
        // remove encryption before signing'
        raise Exception.Create('Decrypt the contract before signing: '+ E.Message);
    end;
  finally
    Pdf.Free;
  end;
end;

Att sekvensera konformitetsstämplar, signaturer, och kryptering

Den praktiska fixen är ordningsföljd, inte ett annat bibliotek. Tillämpa PDF/A-, PDF/X-, PDF/UA-, PDF/E-1-, PDF/R-1-, eller PDF/VT-1-markörer först, lägg till en eventuell PAdES-signatur därefter, och kör först då vad för steg i din pipeline som faktiskt äger kryptering, vare sig det är en dedikerad PDF-skrivare, en signeringsapparat, eller din egen AES-implementation. PDFiumPas inkrementella uppdateringslager passar naturligt in i mitten av den sekvensen, lägger till små, riktade objekt till en fil som annars är klar, och kryptering hör hemma i slutet precis eftersom det är den enda operationen i kedjan PDFiumPas själv varken kan utföra eller vända

Inget av detta ändrar hur PDFiumPas läser efterskrifts- och korsreferensdata som varje inkrementell uppdatering beror på, vilket är en egen källa till subtilitet när xref-strömmar kommer in i bilden; att validera en PDF:s objekt- och xref-strömmar täcker hur den samma efterskriftläsningsvägen hanterar PDF 1.5+ komprimerade strukturer. Och när ett dokument väl är redo för något starkare än en konformitetsstämpel är att signera en PDF med en PAdES B-B-signatur i Delphi där SignPades tar över precis från den punkt där den här artikeln slutar

Markörinjektorerna och SignPades-metoderna som beskrivs här levereras som en del av PDFium-komponenten för Delphi och C++Builder, tillsammans med rendreringen och skrivskyddade inspektionen PDFium tillhandahåller nativt