HotPDF Delphi PDF component имплементира crypt filter model-а от ISO 32000-1 §7.6.5 като три независими policies, а не като един switch: ConfigureCryptFilterDefaults задава отделно string filter-а /StrF, stream filter-а /StmF и embedded-file filter-а /EFF, SetStreamCryptFilter override-ва един stream, а GetLoadedCryptFilterInfo докладва какво декларира входният файл. Повечето interop bug-ове при encrypted PDF живеят в празнините между тези три
Ето failure-а, който изпраща хората към този layer. Team доставя document, в който page content трябва да остане четим за downstream tool, но attached payload не трябва, затова задава /EFF /StdCF и оставя /StmF /Identity. Acrobat го отваря без проблем. Conforming third-party reader връща attachment-а като ciphertext garbage, защото /EFF е producer-side policy за това кой filter се прилага към embedded files, а general reader все пак разрешава unmarked stream през /StmF. Поправката не е друга стойност на /EFF. Поправката е explicit /Crypt filter върху самия embedded-file stream
Какво действително управлява crypt filter layer-ът
Crypt filter-ите стоят между encryption algorithm-а и object graph-а и решават кои objects засяга algorithm-ът, а не как работи той. /CF dictionary вътре в encryption dictionary map-ва names към filter definitions, всяка от които носи /CFM method, optional /Length и /AuthEvent. Трите top-level entry-та /StrF, /StmF и /EFF после избират кой от тези named filters се прилага към strings, към streams без explicit filter и към embedded files. HotPDF умишлено ограничава какво ще write-ват built-in handler-ите му. ConfigureCryptFilterDefaults приема само reserved names за active handler-а: Standard security handler-ът emit-ва /StdCF или /Identity, public-key handler-ът emit-ва /DefaultCryptFilter или /Identity, а всичко друго вдига EArgumentException на call site-а. Filters, записани от external producers под други names, все пак се запазват при load, inspection и compatibility-rewrite paths, така че HotPDF е conservative като writer и permissive като reader. Прилагат се още две guards: call-ът вдига EInvalidOpException, след като document serialization е започнал, и отново, ако document-ът е в incremental update, защото encryption policy не може да се променя между revisions на същия file
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;
// strings са encrypted, page streams са plaintext, attachments са encrypted
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;
Едно ограничение трябва да се каже предварително, защото се проверява късно и изненадва хората. Named crypt filters в HotPDF изискват aes128, aes256 или aesgcm document encryption. Configure-нете filter policy върху RC4 k40 или k128 и validation pass-ът, който работи при enable на encryption, ще вдигне error, вместо тихо да повиши key type-а. Това е същата design stance като в останалата AES-256 PDF encryption path в Delphi: откажете ambiguous configuration, вместо да гадаете какво е имал предвид caller-ът
Защо entry /Length означава две различни неща?
Защото spec-ът го дефинира в две различни units според security handler-а и HotPDF трябва да спазва и двете. В crypt filter dictionary, чийто /CFM е /V2, entry-то /Length е изразено в bytes при Standard security handler и в bits при public-key handler. /Length в encryption dictionary, който стои до /V (ISO 32000-1 §7.6.2), винаги е в bits. Прочетете filter dictionary с /Length 16 и ще имате 128-bit key във файл със Standard handler и rejected file при public-key. HotPDF нормализира това, когато capture-ва loaded configuration-а. Умножава /V2 filter /Length по осем само когато file-ът не е public-key encrypted, fallback-ва към document-level /Length, когато filter-ът пропуска своя, и съхранява резултата в THPDFCryptFilterInfo.KeyLengthBits. AESV2 е pinned до 128 bits, а AESV3 и AESV4 до 256, защото тези methods нямат negotiable key size. Строгата част идва след това: приемат се само 40-bit и 128-bit /V2. Filter, който се разреши до друга дължина, се докладва като unavailable и операцията се проваля, вместо да се закръгли до 128 с предположението, че повечето producers са имали предвид 128. Тихото нормализиране на key length е начин да доставите file, който се decrypt-ва на вашата машина и никъде другаде
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 и /StmF default-ват до Identity; /EFF default-ва до /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;
Какво гарантира /CFM /None и по какво /Identity е различен?
Те стигат до един и същ резултат по различни маршрути и смесването им чупи lookup-ите. Named filter, чийто /CFM е /None, и named filter, който изцяло пропуска /CFM, и двата означават, че този filter не извършва encryption или decryption — HotPDF map-ва липсващия entry към None преди resolving, така че и двата завършват в hcfmNone със записана key length нула. /Identity е различен по вид: това е reserved name, което заобикаля lookup-а в /CF изцяло, така че document може да реферира /Identity, без да го дефинира никъде в /CF. PDF names са case-sensitive, което прави още един implementation detail non-negotiable: никой crypt filter lookup не може да бъде case-insensitive. HotPDF разрешава /CF sub-dictionary names, filter /Length entry и stream /Type check чрез case-sensitive dictionary lookup-и. File, който дефинира /stdcf, докато /StmF сочи към /StdCF, е malformed, а третирането им като един key би превърнало откриваем authoring bug в грешен key, приложен тихо към всеки stream в document-а
Как да накарате /EFF да се приложи към embedded-file stream-овете
Когато /EFF се различава от /StmF, embedded-file stream-ът се нуждае от explicit leading /Crypt entry във /Filter и matching /DecodeParms dictionary с /Name на същата array position. HotPDF изчислява това per stream при save: открива /Type /EmbeddedFile, наследява configured embedded-file filter и emit-ва explicit /Crypt marker само когато inherited name се различава от effective stream default. Когато /EFF и /StmF съвпадат, marker не се записва, защото reader би разрешил същия filter. Array position тогава е толкова важна, колкото и name-ът. Когато HotPDF чете stream обратно, сканира /Filter за /Crypt entry, записва index-а му и след това търси на същия index в /DecodeParms array, за да намери /Name. /Crypt на index 0, paired с parameters на index 1, се разрешава до /Identity, а не до вашия filter. Затова writer-ът добавя null в parameter array, когато stream-ът преди е имал /Filter, но не е имал /DecodeParms: positions трябва да останат подравнени
Под това има още по-остър trap. Ако съществуващият /Filter или /DecodeParms е indirect object — често срещано при files от generators, които споделят един filter array между много streams — вмъкването на /Crypt на място би мутирало shared filter graph и би повредило всеки друг stream, който сочи към него. HotPDF resolve-ва indirect object-а и първо го clone-ва в stream-private direct object, като изчиства object и generation numbers, така че оригиналният indirect root никога да не бъде вграден в новия array. За stream, който вече е използвал ASCIIHexDecode, serialized result-ът е /Filter [ /Crypt /ASCIIHexDecode ] с /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Същата positional discipline управлява всяка друга filter chain, включително тези, които обхождате при извличане на images от loaded PDF чрез decode filters
// Editor вече държи loaded document, а ContentStream е
// THPDFStreamObject, чийто /Filter е indirect /ASCIIHexDecode name
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');
// Празно име изчиства override-а и премахва stale /Crypt
// entry-то заедно с decode parameters при следващия save
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Object streams наследяват ли document /Encrypt policy?
Не и предположението, че го правят, е надежден начин да произведете garbage. Object stream трябва да следва реалната /StmF policy или своя explicit /Crypt marker: самото наличие на /Encrypt dictionary не прави всеки /ObjStm container ciphertext. Document с /StmF /Identity има plaintext object streams, въпреки че strings-овете му са изцяло encrypted, а decoder, който ги decrypt-ва въпреки това, подава на inflate stage input, който никога не е бил deflate output
Последицата за member objects е частта, която си струва да прочетете два пъти. Според ISO 32000-1 §7.5.7 strings вътре в encrypted object stream вече са plaintext, след като самият container бъде decrypted, така че повторното им decrypt-ване би било double-decrypt. HotPDF пази това чрез query дали container-ът на всеки type-2 object е бил encrypted и пропуска object-а, когато е, като брои пропуските в XRefProbeDecryptObjStmSkips като директно evidence, че guard-ът е сработил. Когато container-ът е бил plaintext, member strings никога не са били покрити с нищо, така че HotPDF materialize-ва тези members и прилага /StrF към всеки поотделно — с key-ване, което implementation-ът реално използва, чрез member object number и generation, а не чрез containing /ObjStm object number. Обърнете това при mixed-policy file и всеки string във всеки compressed object ще се decode-не като noise. Container-level rules около това са разгледани допълнително в бележките за PDF object streams и incremental updates
Къде HotPDF отказва да гадае
Crypt filter semantics не съществуват под /V 4, затова HotPDF отхвърля per-stream override върху такъв file с explicit error, вместо да записва /Crypt marker, който никой conforming reader не би уважил. Същото важи и при read: encryption dictionary с /V под 4 изчиства и трите loaded filter names, защото там няма какво да се докладва. Още три граници се налагат нарочно:
- Per-stream filter, различен от
Identity, върху public-key encrypted document се отказва, защото stream-specific policy при public-key handler се нуждае от stream-specific recipient envelope, който HotPDF още не emit-ва - Public-key encrypted embedded files, чиито
/EFFсе различава от effective/StmF, се отказват по същата причина, вместо да бъдат записани във форма, която не се decrypt-ва за никого - AES-256 direct-file fast path се прилага само когато strings, streams и embedded files се разрешават до един и същ crypt filter method и никой object във file-а не носи explicit
/Crypt; mixed policy или plaintext metadata forcing-ва fallback към full object-graph path
Нито едно от тези ограничения не е performance decision. Те маркират местата, където грешен guess дава PDF, който се отваря в един viewer, проваля се в друг и не дава на developer-а никакъв signal, докато customer не съобщи. Refusal при ConfigureCryptFilterDefaults или при save струва един exception; silently mis-key-нат embedded file струва support cycle. Ако изграждате Delphi или C++Builder software, което произвежда или консумира encrypted PDF — selectively plaintext page content с encrypted attachments, PDF 2.0 encrypted payload wrappers или interop с files, чиито crypt filter policies не сте избрали — crypt filter API-то, описано тук, е част от текущия HotPDF Delphi PDF component, заедно с encryption, object stream и incremental update paths, върху които се основава