Техническа статия

Не можете тихомълком да промените шифрован PDF в Delphi

Вземете фактурен PDF, вече защитен с AES-256, и поискайте от PDFium Component за Delphi и C++Builder (PDFiumPas) да го маркира като PDF/A за архивиране или да го подпише с PAdES чрез инкрементална актуализация вместо чрез пълно презаписване. Библиотеката няма да постигне това чрез директна промяна на шифрованите байтове: шестте ѝ инжектора за маркери откриват съществуващ запис /Encrypt и препредават изходния файл байт по байт без промяна, а подписващият код за PAdES хвърля изключение, вместо да издаде подпис, който никой валидатор няма да приеме

Това е различно от одита на PDF, който не сте създали, за скрити рискове, защото той е отделна операция само за четене. Тази статия разглежда страната на записването в същата граница на доверие: какво има право да направи вашият код с файл, чиито байтове вече са заключени с чужда парола, когато се опита да добави нещо след създаването му

Какво изисква ISO 32000-1 при инкрементална актуализация на шифрован PDF

ISO 32000-1 §7.5.6 изисква трейлърът на инкременталната актуализация да повтори всеки запис от предишния трейлър с изключение на /Prev, а Table 15 посочва /Encrypt сред записите, които трейлърът може да съдържа. Премахнете го от новия трейлър и съвместимият четец няма причина да се съмнява в пропуска: най-новият трейлър е водещ, затова четецът решава, че целият файл без /Encrypt е нешиврован, и се опитва да обработи старото, все още шифровано тяло като обикновени байтове. Запазете /Encrypt в новия трейлър, но запишете обектите на актуализацията като обикновен текст, и грешката просто се появява една стъпка по-късно: четецът правилно открива шифроването, пропуска всеки използван обект през шифъра на файла, включително новите обекти, които никога не са били шифровани, и получава безсмислени данни там, където преди декриптирането всичко е било четливо. И в двата случая на ниво байтове файлът изглежда като нормална, добре оформена инкрементална актуализация, докато съвместим четец не го отвори

Шест инжектора за маркери и една проверка за шифроване във v2.14.2

PDFiumPas предоставя шест инжектора за маркери на ниво байтове, по един за всеки ISO профил на PDF, който може да обозначи: 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) и PDF/VT-1 (ISO 16612-2). Всеки взема байтовете, които собственото FPDF_SaveAsCopy на PDFium вече е записало, и добавя върху тях втора, по-малка инкрементална актуализация: нов поток с XMP метаданни, редакция на речника на каталога, която сочи към него, а за профилите, предназначени за печат, и OutputIntent с ICC профил. От v2.14.2 всеки от InjectPdfAMarkers, InjectPdfXMarkers, InjectPdfUaMarkers, InjectPdfEMarkers, InjectPdfRMarkers и InjectPdfVTMarkers първо прочита трейлъра на източника и ако той съдържа запис /Encrypt, копира източника в целевия поток без промяна и незабавно приключва. Няма XMP, няма OutputIntent и няма промяна на каталога — извикващият получава оригиналния файл байт по байт

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;

Това, че PDF може да е шифрован, не означава, че е безопасен за инжектиране

PDF/E-1 и PDF/R-1 изрично допускат хостващият документ да бъде шифрован на ниво спецификация, което изглежда като изключение, докато не се види какво трябва реално да се случи на диска. ISO 24517-1 §6.3 разрешава шифроване за PDF/E-1, а ISO 23504-1 §6.2.3 го разрешава за PDF/R-1, ако заглавката обявява %PDF-2.0. Нито една от тези клаузи не казва, че обработчик на ниво байтове може безопасно да добави обект с обикновен текст към този шифрован контейнер, а това е невъзможно по същите причини от §7.5.6, които важат за всеки друг профил. Собствените валидатори за съответствие на PDFiumPas за тези два профила, ValidatePdfECompliance и ValidatePdfRCompliance, умишлено регистрират наличието на /Encrypt, без да го отбелязват като дефект, което е правилно за валидатор само за четене, който не записва нито един байт. Лесно е обаче този модел да бъде пропуснат и да се приеме, че съответният инжектор няма нужда от отделна проверка, въпреки че именно инжекторът от двойката трябва да откаже операцията

SaveAsPdfX тихомълком ли декриптира документа

Да, когато използвате публичните удобни методи вместо инжектор директно. Всеки от TPdf.SaveAsPdfA, SaveAsPdfX, SaveAsPdfUa, SaveAsPdfE, SaveAsPdfR и SaveAsPdfVT визуализира текущия документ във временен поток чрез SaveAs(Tmp, saRemoveSecurity), преди да подаде тези байтове на съответния инжектор. saRemoveSecurity съответства на собствения флаг FPDF_REMOVE_SECURITY на PDFium, така че временната копия, която инжекторът получава, изобщо не е била шифрована и проверката му за /Encrypt няма причина да се задейства. Резултатът съдържа вашите маркери за PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 или PDF/VT-1, но вече не е защитен с паролата, отворила изходния документ

Този компромис остава невидим, докато някой не отвори уж защитеното архивно копие без парола и не установи, че то просто се отваря. Решението не е друг извикван метод: PDFiumPas няма еквивалент на saAddSecurity, който да се съчетае със saRemoveSecurity, защото базовият PDFium никога не е бил предназначен да записва ново шифроване, а само да го премахва. Ако един файл трябва да има и двете свойства, шифроването трябва да бъде отделна стъпка във вашия процес, изпълнена след маркерите за съответствие, а не скрита в същото извикване на SaveAsPdfA

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;

Какво става при подписване на шифрован PDF с PAdES

PDFiumPas отказва директно, вместо тихомълком да пропусне заявката, както прави инжекторът за маркери. TPdf.SignPades и SignPadesToStream минават през вътрешния SignPadesBytes, който след анализа на трейлъра на източника първо проверява за /Encrypt. Ако записът е налице, той хвърля EPadesCrypto със съобщението „SignPadesBytes: the source document is encrypted; remove encryption before signing“, вместо да продължи. InjectPadesDssMarkers, функцията, която вгражда сертификати, OCSP отговори и CRL за дългосрочна проверка, извършва същата проверка по същата причина и използва собственото си съобщение „InjectPadesDssMarkers: the source document is encrypted; remove encryption before embedding DSS validation material“

Тук разсъждението е по-строго от пропускането на операцията при инжекторите за маркери и това е нарочно. Тихото пропускане е безопасно при печат за PDF/A, защото пропускът оставя същия валиден PDF, който сте започнали с него, само без етикет. Подписването не може да се провали толкова тихо: подпис, който не е добавен без предупреждение, изглежда напълно същото за извикващия код, ако той проверява само булев резултат, като подпис, който е добавен успешно. EPadesCrypto произлиза от обичайния клас Exception, така че обработването му е стандартна обработка на изключение, а не специален модел за управление на потока

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;

Подредба на маркерите за съответствие, подписите и шифроването

Практическото решение е правилният ред, а не друг метод на библиотеката. Първо приложете маркерите за PDF/A, PDF/X, PDF/UA, PDF/E-1, PDF/R-1 или PDF/VT-1, след това добавете PAdES подпис, а накрая изпълнете стъпката от процеса, която действително отговаря за шифроването, независимо дали това е специализиран PDF записвач, устройство за подписване или собствена AES реализация. Инкременталният слой на PDFiumPas естествено заема средната позиция, като добавя малки, целеви обекти към вече завършен файл, а шифроването е накрая именно защото е единствената операция във веригата, която самият PDFiumPas не може да извърши или обърне

Това не променя начина, по който PDFiumPas чете трейлъра и данните за кръстосаните препратки, от които зависи всяка инкрементална актуализация, а това е отделен източник на тънкости, когато се появят потоци xref; валидиране на обектните и xref потоците в PDF проверява как същият път за четене на трейлъра обработва компресираните структури на PDF 1.5 и по-нови версии. Когато документът е готов за нещо по-силно от маркер за съответствие, подписване на PDF с PAdES B-B подпис в Delphi е мястото, където SignPades продължава от точката, на която тази статия спира

Описаните тук инжектори за маркери и методи SignPades са част от PDFium Component за Delphi и C++Builder, заедно с визуализирането и проверката само за четене, които PDFium предоставя вградено