Komponent PDF HotPDF dla Delphi implementuje model filtrów szyfrowania z ISO 32000-1 §7.6.5 jako trzy niezależne polityki, a nie jeden przełącznik: ConfigureCryptFilterDefaults osobno przypisuje filtr łańcuchów /StrF, filtr strumieni /StmF i filtr osadzonych plików /EFF, SetStreamCryptFilter nadpisuje pojedynczy strumień, a GetLoadedCryptFilterInfo raportuje deklarację pliku wejściowego. Większość problemów interoperacyjności zaszyfrowanych PDF-ów żyje właśnie w lukach między tymi trzema elementami
Oto awaria, która kieruje ludzi do tej warstwy. Zespół dostarcza dokument, w którym treść strony ma pozostać czytelna dla narzędzia downstream, ale dołączony ładunek nie, więc ustawia /EFF /StdCF i pozostawia /StmF /Identity. Acrobat otwiera plik poprawnie. Zgodny czytnik zewnętrzny zwraca załącznik jako śmieciowy szyfrogram, ponieważ /EFF jest polityką po stronie producenta określającą filtr dla plików osadzonych, a ogólny czytnik nadal rozwiązuje nieoznaczony strumień przez /StmF. Poprawką nie jest inna wartość /EFF. Poprawką jest jawny filtr /Crypt bezpośrednio na strumieniu osadzonego pliku
Co naprawdę kontroluje warstwa filtrów szyfrowania
Filtry szyfrowania znajdują się między algorytmem szyfrowania a grafem obiektów i decydują, których obiektów algorytm dotknie, a nie jak algorytm działa. Słownik /CF wewnątrz słownika szyfrowania mapuje nazwy na definicje filtrów, z których każda ma metodę /CFM, opcjonalne /Length i /AuthEvent. Trzy wpisy najwyższego poziomu /StrF, /StmF i /EFF wybierają następnie nazwany filtr dla łańcuchów, dla strumieni bez jawnego filtra oraz dla plików osadzonych. HotPDF celowo ogranicza to, co jego wbudowane handlery będą zapisywać. ConfigureCryptFilterDefaults przyjmuje wyłącznie zarezerwowane nazwy aktywnego handlera: Standard security handler emituje /StdCF albo /Identity, handler klucza publicznego emituje /DefaultCryptFilter albo /Identity, a każda inna wartość wywołuje EArgumentException w miejscu wywołania. Filtry zapisane przez zewnętrznych producentów pod innymi nazwami są jednak zachowywane podczas ładowania, inspekcji i zapisu kompatybilności, więc HotPDF jest konserwatywny jako writer i pobłażliwy jako reader. Obowiązują jeszcze dwa zabezpieczenia: wywołanie zgłasza EInvalidOpException po rozpoczęciu serializacji dokumentu oraz ponownie, gdy dokument jest aktualizacją przyrostową, ponieważ polityki szyfrowania nie można zmieniać między rewizjami tego samego pliku
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;
// łańcuchy zaszyfrowane, strumienie stron jawne, załączniki zaszyfrowane
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;
Warto od razu nazwać jedno ograniczenie, ponieważ jest sprawdzane późno i zaskakuje użytkowników. Nazwane filtry szyfrowania w HotPDF wymagają szyfrowania dokumentu aes128, aes256 albo aesgcm. Skonfiguruj politykę filtra na RC4 k40 albo k128, a walidacja wykonywana przy włączeniu szyfrowania zgłosi błąd zamiast po cichu promować typ klucza. To ta sama postawa projektowa, która obowiązuje w całej ścieżce szyfrowania PDF AES-256 w Delphi: odrzucić niejednoznaczną konfigurację, zamiast zgadywać, co wywołujący miał na myśli
Dlaczego wpis /Length oznacza dwie różne rzeczy?
Bo specyfikacja definiuje go w dwóch różnych jednostkach zależnie od handlera bezpieczeństwa, a HotPDF musi respektować oba znaczenia. W słowniku filtra szyfrowania, którego /CFM to /V2, wpis /Length jest wyrażony w bajtach w Standard security handlerze i w bitach w handlerze klucza publicznego. /Length w słowniku szyfrowania, umieszczony obok /V (ISO 32000-1 §7.6.2), zawsze podaje bity. Odczytaj słownik filtra z /Length 16, a otrzymasz klucz 128-bitowy w pliku z handlerem Standard i odrzucony plik w wariancie klucza publicznego. HotPDF normalizuje to podczas przechwytywania załadowanej konfiguracji. Mnoży /Length filtra /V2 przez osiem wyłącznie wtedy, gdy plik nie jest zaszyfrowany kluczem publicznym, korzysta z /Length na poziomie dokumentu, gdy filtr nie ma własnego wpisu, i zapisuje wynik w THPDFCryptFilterInfo.KeyLengthBits. AESV2 jest przypięty do 128 bitów, a AESV3 i AESV4 do 256, ponieważ te metody nie mają negocjowalnego rozmiaru klucza. Dalej pojawia się rygorystyczna część: akceptowane są tylko 40-bitowe i 128-bitowe /V2. Filtr rozwiązujący się do dowolnej innej długości jest raportowany jako niedostępny, a operacja kończy się błędem zamiast zaokrąglania do 128 na podstawie założenia, że większość producentów miała na myśli 128. Ciche normalizowanie długości klucza prowadzi do pliku, który odszyfrowuje się na twoim komputerze i nigdzie indziej
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 i /StmF domyślnie wskazują Identity; /EFF domyślnie wskazuje /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;
Co gwarantuje /CFM /None i czym różni się /Identity?
Do tego samego wyniku dochodzą różnymi drogami, a pomieszanie ich psuje wyszukiwanie. Nazwany filtr, którego /CFM to /None, oraz nazwany filtr bez /CFM oznaczają, że filtr nie wykonuje szyfrowania ani deszyfrowania — HotPDF mapuje brakujący wpis na None przed rozwiązaniem, więc oba kończą jako hcfmNone z zapisanym rozmiarem klucza równym zero. /Identity jest czymś innym: to zarezerwowana nazwa, która całkowicie omija wyszukiwanie w /CF, dlatego dokument może wskazywać /Identity bez definiowania go w /CF. Nazwy PDF są wrażliwe na wielkość liter, co czyni jeszcze jeden szczegół implementacyjny niepodlegającym negocjacji: żadne wyszukiwanie filtra szyfrowania nie może być niewrażliwe na wielkość liter. HotPDF rozwiązuje nazwy pod-słownika /CF, wpis /Length filtra i sprawdzenie /Type strumienia przez wyszukiwania wrażliwe na wielkość liter. Plik definiujący /stdcf, gdy /StmF wskazuje /StdCF, jest wadliwy, a potraktowanie tych dwóch nazw jako tego samego klucza zamieniłoby wykrywalny błąd autora w ciche zastosowanie złego klucza do każdego strumienia dokumentu
Jak sprawić, by /EFF obowiązywał na strumieniach plików osadzonych
Gdy /EFF różni się od /StmF, strumień osadzonego pliku potrzebuje jawnego początkowego wpisu /Crypt w swoim /Filter oraz pasującego słownika /DecodeParms zawierającego /Name na tej samej pozycji tablicy. HotPDF oblicza to dla każdego strumienia przy zapisie: wykrywa /Type /EmbeddedFile, dziedziczy skonfigurowany filtr pliku osadzonego i emituje jawny znacznik /Crypt tylko wtedy, gdy odziedziczona nazwa różni się od efektywnego domyślnego filtra strumienia. Gdy /EFF i /StmF są zgodne, znacznik nie jest zapisywany, ponieważ czytnik rozwiązałby ten sam filtr. Pozycja w tablicy jest więc równie ważna jak nazwa. Podczas ponownego odczytu strumienia HotPDF skanuje /Filter w poszukiwaniu wpisu /Crypt, zapisuje jego indeks, a następnie szuka tego samego indeksu w tablicy /DecodeParms, aby znaleźć /Name. /Crypt na indeksie 0 połączony z parametrami na indeksie 1 rozwiązuje się do /Identity, a nie do twojego filtra. Dlatego writer wypełnia tablicę parametrów wartością null, gdy strumień miał wcześniej /Filter, ale nie miał /DecodeParms: pozycje muszą pozostać wyrównane
Pod spodem kryje się ostrzejsza pułapka. Jeśli istniejący /Filter albo /DecodeParms jest obiektem pośrednim — co często zdarza się w plikach z generatorów współdzielących jedną tablicę filtrów między wieloma strumieniami — wstawienie /Crypt na miejscu zmieniłoby współdzielony graf filtrów i uszkodziło każdy inny strumień, który na niego wskazuje. HotPDF rozwiązuje obiekt pośredni i najpierw klonuje go do bezpośredniego obiektu prywatnego dla strumienia, czyszcząc numery obiektu i generacji, aby pierwotny korzeń pośredni nigdy nie został osadzony w nowej tablicy. Dla strumienia, który już używał ASCIIHexDecode, serializowany wynik to /Filter [ /Crypt /ASCIIHexDecode ] z /DecodeParms [ << /Type /CryptFilterDecodeParms /Name /StdCF >> ... ]. Ta sama dyscyplina pozycyjna obowiązuje każdy inny łańcuch filtrów, w tym te, po których poruszasz się podczas wyodrębniania obrazów z załadowanego PDF-a przez ich filtry dekodujące
// Editor przechowuje już załadowany dokument, a ContentStream to
// THPDFStreamObject, którego /Filter jest pośrednią nazwą /ASCIIHexDecode
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');
// Pusta nazwa usuwa nadpisanie i przy następnym zapisie usuwa nieaktualny
// wpis /Crypt razem z jego parametrami dekodowania
Editor.SetStreamCryptFilter(ContentStream, '');
Editor.SaveLoadedDocument('cleared.pdf');
Czy strumienie obiektów dziedziczą politykę dokumentu /Encrypt?
Nie, a założenie, że tak jest, to niezawodny sposób na wygenerowanie śmieci. Strumień obiektów musi stosować rzeczywistą politykę /StmF albo własny jawny znacznik /Crypt: sama obecność słownika /Encrypt nie sprawia, że każdy kontener /ObjStm staje się szyfrogramem. Dokument z /StmF /Identity ma jawne strumienie obiektów, nawet gdy jego łańcuchy są w pełni zaszyfrowane, a dekoder, który mimo to je odszyfruje, przekaże do fazy inflate dane, które nigdy nie były wynikiem deflate
Konsekwencja dla obiektów członkowskich jest warta przeczytania dwa razy. Zgodnie z ISO 32000-1 §7.5.7 łańcuchy wewnątrz zaszyfrowanego strumienia obiektów są już jawne po odszyfrowaniu samego kontenera, więc ponowne ich odszyfrowanie byłoby podwójnym deszyfrowaniem. HotPDF chroni się przed tym, sprawdzając, czy kontener każdego obiektu typu 2 był zaszyfrowany, i pomijając obiekt, gdy był, a pominięcia zlicza w XRefProbeDecryptObjStmSkips jako bezpośredni dowód zadziałania zabezpieczenia. Gdy kontener był jawny, łańcuchy członków nigdy nie były niczym objęte, więc HotPDF materializuje te elementy i stosuje /StrF do każdego z osobna — z kluczem, tak jak faktycznie robi implementacja, opartym na numerze i generacji obiektu członkowskiego, a nie numerze zawierającego go obiektu /ObjStm. Odwróć tę zasadę w pliku z mieszaną polityką, a każdy łańcuch w każdym skompresowanym obiekcie zdekoduje się do szumu. Reguły na poziomie kontenera są szerzej omówione w notatkach o strumieniach obiektów PDF i aktualizacjach przyrostowych
Gdzie HotPDF odmawia zgadywania
Semantyka filtrów szyfrowania nie istnieje poniżej /V 4, więc HotPDF odrzuca każde nadpisanie per strumień w takim pliku jawnym błędem zamiast zapisywać znacznik /Crypt, którego nie honorowałby żaden zgodny czytnik. To samo obowiązuje po stronie odczytu: słownik szyfrowania z /V poniżej 4 czyści wszystkie trzy załadowane nazwy filtrów, ponieważ nie ma tam nic do raportowania. Trzy dalsze granice są wymuszane celowo:
- Filtr per strumień inny niż
Identityw dokumencie zaszyfrowanym kluczem publicznym jest odrzucany, ponieważ polityka specyficzna dla strumienia w handlerze klucza publicznego wymaga osobnej koperty odbiorcy dla strumienia, której HotPDF jeszcze nie emituje - Osadzone pliki zaszyfrowane kluczem publicznym, których
/EFFróżni się od efektywnego/StmF, są odrzucane z tego samego powodu zamiast zapisu w postaci, której nikt nie może odszyfrować - Szybka ścieżka bezpośrednia AES-256 dla pliku działa wyłącznie wtedy, gdy łańcuchy, strumienie i pliki osadzone rozwiązują się do tej samej metody filtra szyfrowania i żaden obiekt w pliku nie zawiera jawnego
/Crypt; mieszana polityka albo jawne metadane wymuszają fallback do pełnej ścieżki grafu obiektów
Żadne z tych ograniczeń nie jest decyzją wydajnościową. Wyznaczają miejsca, w których błędne zgadywanie tworzy PDF otwierający się w jednym viewerze, zawodzący w innym i nie dający deweloperowi żadnego sygnału aż do zgłoszenia od klienta. Odmowa w ConfigureCryptFilterDefaults albo przy zapisie kosztuje jeden wyjątek; ciche zastosowanie złego klucza do osadzonego pliku kosztuje cały cykl wsparcia. Jeśli tworzysz oprogramowanie Delphi albo C++Builder produkujące lub konsumujące zaszyfrowane PDF-y — selektywnie jawne treści stron z zaszyfrowanymi załącznikami, zaszyfrowane opakowania ładunków PDF 2.0 albo interoperacyjność z plikami o nieznanych ci politykach filtrów — opisane tutaj API filtrów szyfrowania jest częścią aktualnego komponentu PDF HotPDF dla Delphi, obok ścieżek szyfrowania, strumieni obiektów i aktualizacji przyrostowych, na których się opiera