Artykuł techniczny

Flagi uprawnień PDF, liczba /P i kopia /Perms

Szyfrujesz raport maską mającą pozwalać na wszystko, $FFFFFFF0, a zakładka Security Acrobata wypisuje drukowanie i zmienianie dokumentu jako niedozwolone. /P to 32-bitowe słowo flag bez znaku numerowane od bitu 1, więc ta maska czyści druk (bit 3) i modyfikację (bit 4); wartość wszystko-pozwalająca to $FFFFFFFC, punkt startowy TPdfEncryptOptions w PDFium Component

Jak jest rozłożone słowo flag /P

Tylko osiem pozycji niesie znaczenie, reszta jest zarezerwowana, a liczenie startuje od 1. ISO 32000-1 §7.6.3.2 mówi, że P "ma być interpretowana jako 32-bitowa wielkość bez znaku", numerowana od 1 na końcu niskich bitów do 32 na końcu wysokich, i każe zgodnym czytnikom ignorować każdy bit poza 3, 4, 5, 6, 9, 10, 11 i 12. ISO 32000-2 §7.6.4.2 prostuje tabelę 22 i wypowiada pozycje zarezerwowane: bity 1–2 muszą być 0, bity 7–8 muszą być 1, a od rewizji 3 wzwyż bity 13–32 muszą być 1. PDF 2.0 wycofuje też bit 10, który czytniki teraz ignorują, a writerzy zawsze ustawiają na 1 dla starszych czytników; wyłamanie dla dostępności przenosi się na bit 5, więc czytnik powinien działać tak, jakby kopiowanie było pozwolone, gdy zawartość idzie do technologii wspomagających. Jedna formuła pokrywa każdy wiersz poniżej. Maska bitu n to 1 shl (n - 1) — i TPdfPermission mapuje się na słowo tak samo

BitMaskaZnaczenie (rewizja 3 i późniejsze)TPdfPermission
1–2$00000003Zarezerwowane, muszą być 0brak
3$00000004Druk, w jakości zależnej od bitu 12permPrint
4$00000008Modyfikacja zawartości operacjami innymi niż te z bitów 6, 9 i 11permModify
5$00000010Kopiowanie albo ekstrakcja tekstu i grafikipermCopy
6$00000020Dodawanie albo modyfikacja adnotacji, wypełnianie pól; z bitem 4, tworzenie pólpermAnnotate
7–8$000000C0Zarezerwowane, muszą być 1brak
9$00000100Wypełnianie istniejących pól formularza, łącznie z polami podpisu, nawet gdy bit 6 jest wyzerowanypermFillForms
10$00000200Ekstrakcja dla dostępności; nieużywane w PDF 2.0, zawsze zapisywane jako 1permAccessibility
11$00000400Składanie: wstawianie, obracanie albo usuwanie stron, tworzenie elementów konspektu i miniaturpermAssemble
12$00000800Druk do wiernej, wysokiej jakości reprezentacjipermPrintHighQuality
13–32$FFFFF000Zarezerwowane, muszą być 1 (ISO/TS 32004 przydziela bit 13, patrz niżej)brak

Maska $FFFFFFF0 bierze się z przeczytania "niskie bity są zarezerwowane" i wyzerowania całego nibble'a dla bezpieczeństwa. Region zarezerwowany to były tylko bity 1–2, maska $3; czyszczenie $F kasuje też $4 i $8, czyli druk i modyfikację, i każda przeglądarka honorująca flagi odmówi obu. Błąd lustrzany to składanie maski od zera w górę: zOR-uj razem $4, $10 i $800 i dostaniesz małe dodatnie /P z wyzerowanymi bitami 7–8 i 13–32. Większość czytników otworzy ten plik bez komentarza, bo specyfikacja każe im ignorować bity zarezerwowane, ale wartość jest niezgodna dla rewizji 3 i późniejszych i surowy kontroler ma prawo się przyczepić. Trzeci wariant pojawia się w kodzie liczącym bity od zera, gdzie "bit 3" staje się $8 i program przyznaje modyfikację, choć chciał druku. Żaden z tych przypadków nie produkuje błędu przy zapisie. Uprawnienia po prostu wychodzą inne niż te, jakie zamierzał autor

Rozkład słowa flag uprawnień /P PDF dla PDFium Component w Delphi: bity zarezerwowane 1-2, 7-8 i 13-32, bity TPdfPermission jak permPrint i permModify oraz maski $FFFFFFFC i $FFFFFFF0
Wyczyszczenie całego niskiego nibble'a przez $FFFFFFF0 kasuje też druk i modyfikację, a składanie maski od zera zostawia wyzerowane bity zarezerwowane rewizji 3 i późniejszych

Czy /P -4 to ta sama wartość co /P 4294967292?

Tak. Te same 32 bity, dwie pisownie. Obiekty całkowite PDF są ze znakiem, a notka tuż nad tabelą 22 zwraca uwagę, że skoro zarezerwowane bity wysokich pozycji są 1, liczba całkowita jest zapisywana jako ujemna; -4 to konwencjonalna wartość wszystko-pozwalająca dla rewizji 3 i późniejszych. Producenci trzymający wartość w zmiennej bez znaku zapisują 4294967292, i obie pisownie wyrastają w prawdziwych plikach. Specyfikacja rozstrzyga, które czytanie jest właściwe, we własnym wyprowadzaniu klucza: krok (d) Algorithm 2 w ISO 32000-1 §7.6.3.3 konwertuje P na 32-bitową liczbę binarną bez znaku i podaje cztery bajty do MD5 od bajtu niskiego, więc dla rewizji 2 do 4 wartości -3904 i 4294963392 produkują ten sam klucz pliku. Parser trzymający /P w 32-bitowym Integer ze znakiem odrzuca pisownię bez znaku jako przepełnienie; przełącz zmienną na Cardinal i zamiast tego pada pisownia ujemna, a kod porównujący tekst tokenu z zapamiętaną wartością skończy z traktowaniem jednego dokumentu jako dwóch. Czytaj token do Int64, przyjmuj wszystko od Low(Integer) do High(Cardinal) i maskuj do 32 bitów przed jakimkolwiek porównaniem. Własny kod odszyfrowywania PDFium Component czyta /P tak dla rewizji 2 przez 7, a jego writer wysyła formę ze znakiem, którą pokazuje notka specyfikacji. Raport bitów zarezerwowanych we fragmencie staje na bicie 14, bo wyzerowany bit 13 to sposób, w jaki ISO/TS 32004 oznacza plik niosący PDF MAC

uses
  System.SysUtils;

// Normalizuj tekst tokenu /P ze swojego parsera słownika
function TryNormalizePermissions(const Token: string;
  out Mask: Cardinal): Boolean;
var
  Value: Int64;
begin
  Result := TryStrToInt64(Token, Value) and
    (Value >= Low(Integer)) and (Value <= High(Cardinal));
  if Result then
    Mask := Cardinal(Value and $FFFFFFFF);
end;

function DescribeReservedBits(Mask: Cardinal): string;
begin
  Result := '';
  if (Mask and $00000003) <> 0 then
    Result := Result + 'bits 1-2 set; ';
  if (Mask and $000000C0) <> $000000C0 then
    Result := Result + 'bits 7-8 clear; ';
  // Bit 13 ($1000) zostaje poza: ISO/TS 32004 go czyści, by wymagać PDF MAC
  if (Mask and $FFFFE000) <> $FFFFE000 then
    Result := Result + 'bits 14-32 not all set; ';
end;

// Oczekiwane: oba wołania dają $FFFFFFFC i pusty raport bitów zarezerwowanych
//   TryNormalizePermissions('-4', M1);
//   TryNormalizePermissions('4294967292', M2);
Normalizacja wartości /P PDF w Delphi tak, jak robi to PDFium Component: /P -4 i /P 4294967292 czytane do Int64, sprawdzone w zakresie od Low(Integer) do High(Cardinal) i zamaskowane do Cardinal $FFFFFFFC
Parser Integer ze znakiem i parser Cardinal każdy odrzuca jedną legalną pisownię /P, a czytanie Int64 z maskowaniem do 32 bitów mapuje obie pisownie na jedną wartość

Budowanie maski ze zbioru TPdfPermissions

Startuj z każdym bitem zarezerwowanym na jego wymaganej wartości i OR-uj wyłącznie to, co przyznajesz, więc zapomniane uprawnienie może zamienić się wyłącznie w odmowę. TPdfEncryptOptions.Default w unit FPdfEncrypt już trzyma $FFFFFFFC, co pozwala na wszystko, i wyzerowanie jednego czy dwóch bitów wystarcza do szybkiego ograniczenia. Polityka wypisująca, co odbiorcy mogą robić, czyta się lepiej jako zbiór, a stała tablica poniżej ustawia się w linii z pozycjami bitów, których TPdf używa do dekodowania słowa. Wymuszaj bit 10 niezależnie od tego, co przekaże wołający. ISO 32000-2 każe writerom zawsze go ustawiać, czytniki PDF 2.0 i tak go ignorują, a pozostawienie wyzerowanego ryzykuje tylko zablokowanie czytników ekranu w starszym oprogramowaniu

uses
  System.SysUtils, PDFium, FPdfEncrypt;

const
  PermissionBit: array[TPdfPermission] of Cardinal = (
    $0004,   // permPrint             bit 3
    $0008,   // permModify            bit 4
    $0010,   // permCopy              bit 5
    $0020,   // permAnnotate          bit 6
    $0100,   // permFillForms         bit 9
    $0200,   // permAccessibility     bit 10
    $0400,   // permAssemble          bit 11
    $0800);  // permPrintHighQuality  bit 12

  // Bity 7-8 i 13-32 ustawione; bity 1-2 i każdy bit uprawnień wyzerowany
  ReservedOnes = Cardinal($FFFFF0C0);

function PermissionsToMask(Allowed: TPdfPermissions): Cardinal;
var
  P: TPdfPermission;
begin
  Result := ReservedOnes or PermissionBit[permAccessibility];
  for P in Allowed do
    Result := Result or PermissionBit[P];
end;

procedure SaveFillAndPrintCopy(const Source, Target: string;
  const OwnerPassword: AnsiString);
var
  Pdf: TPdf;
  Opts: TPdfEncryptOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := Source;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.CreateFmt('Could not open %s', [Source]);

    Opts := TPdfEncryptOptions.Default;   // Revision = erR5 tutaj
    Opts.UserPassword := '';              // każdy może otworzyć plik
    Opts.OwnerPassword := OwnerPassword;  // strzeże zbioru uprawnień
    Opts.Revision := erR6;
    Opts.Permissions := PermissionsToMask(
      [permPrint, permPrintHighQuality, permFillForms]);

    // Oczekiwane: /P -1084 = 4294966212 ($FFFFFBC4)
    Writeln('/P ', Integer(Opts.Permissions), ' = ', Opts.Permissions);

    if not Pdf.SaveAsEncrypted(Target, Opts) then
      raise Exception.Create('Encrypted save failed');
  finally
    Pdf.Free;
  end;
end;

Oczekiwane: słownik Encrypt wyniku niesie /P -1084, a przeglądarka honorująca flagi oferuje druk w pełnej jakości i wypełnianie formularzy, przy wyszarzeniu edycji zawartości, kopiowania, komentowania i składania stron. Bit 13 to jedna pozycja, którą writer zarządza za ciebie. Przy EnableIntegrityProtection zostawionym na False SaveAsEncrypted ustawia bit 13, cokolwiek mówi twoja maska; przy ustawionym na True writer czyści bit 13 i w /P, i w /Perms. Pozostałe bity zarezerwowane zostają twoją odpowiedzialnością — stąd punkt startowy $FFFFF0C0. Przykład wybiera erR6, bo R6 to rewizja AES-256, którą standardyzuje ISO 32000-2; domyślny rekord niesie erR5

/Perms, zaszyfrowana kopia /P w plikach AES-256

W rewizjach 2 do 4 słowo uprawnień jest przyspawane do klucza: zmień jeden bit /P, a wejście MD5 się zmienia, wyprowadzony klucz jest zły i nic się nie odszyfrowuje. Pliki AES-256 zrywają to łącze. Algorithm 2.A w ISO 32000-2 §7.6.4.3.3 odpakowuje klucz szyfrowania pliku z /UE albo /OE, a P nie gra w tym wyprowadzaniu żadnej roli, więc sama w sobie jawna /P mogłaby być edytowana dowolnie. /Perms zamyka tę lukę. Algorithm 10 (§7.6.4.4.9) napełnia blok 16 bajtów i szyfruje go kluczem pliku AES-256 w trybie ECB:

  • bajty 0–3: słowo uprawnień, od bajtu niskiego, rozciągnięte do 64 bitów przez bajty 4–7, wszystkie $FF
  • bajt 8: ASCII T albo F, pasujące do /EncryptMetadata
  • bajty 9–11: znacznik ASCII adb
  • bajty 12–15: dane losowe, które czytniki ignorują

Krok (f) Algorithm 2.A odszyfrowuje blok, weryfikuje adb i wymaga, by bajty 0–3 pasowały do P — shall, nie should. Algorithm 13 (§7.6.4.4.12) powtarza kontrole znacznika i uprawnień z łagodniejszym should i dokłada jedną: bajt 8 wobec EncryptMetadata. PDFium Component czyta to surowo wszędzie tam, gdzie własny kod otwiera warstwę szyfrowania: odszyfrowywanie AESV4, walidacja PDF MAC i zapisy przyrostowe do plików zaszyfrowanych. Po tym, jak hasło user albo owner odblokuje klucz pliku, ten kod porównuje wszystkie cztery bajty uprawnień z znormalizowaną /P, sprawdza wypełniacz $FF, bajt T/F i znacznik, i obala wczytanie z "PDF encryption permissions validation failed" przy każdej różnicy, bo rozjazd znaczy, że ktoś zedytował słownik po zaszyfrowaniu i żadnej z obu kopii nie wolno brać za dobrą

Blok /Perms AES-256 w PDFium Component: słowo uprawnień /P od bajtu niskiego, wypełniacz $FF, bajt /EncryptMetadata T albo F i znacznik adb, zaszyfrowany Algorithm 10 i weryfikowany pod Algorithms 2.A i 13
Skoro klucz pliku AES-256 nie zależy już od /P, to odszyfrowane bajty /Perms dowodzą, że jawne słowo uprawnień nie zostało zedytowane po zaszyfrowaniu

Porównanie EncryptMetadata zasługuje na staranność w każdym czytniku, jaki napiszesz. Skrót typu Pos('/EncryptMetadata false', Dict) > 0 gubi /EncryptMetadata false z dwiema spacjami, łamanie wiersza między kluczem a wartością albo komentarz pomiędzy — wszystko to legalna składnia PDF. Czytnik przyjmuje wtedy domyślne true, znajduje F w zupełnie poprawnym /Perms i odrzuca plik. Poprawny plik, odrzucony. Parsuj klucz i jego wartość jako tokeny, a wartość nie-boolean traktuj jako uszkodzoną, zamiast podstawiać domyślną

ISO/TS 32004 daje bitowi 13 robotę: gdy jest wyzerowany, dokument niesie PDF MAC, który czytniki muszą sprawdzić. Token MAC siedzi w słowniku /AuthCode trailera, który nie jest zaszyfrowany, więc kontrola szukająca wyłącznie /AuthCode uzna, że plik z usuniętym wpisem nigdy nie był chroniony integralnościowo. Usunięcie przy okazji /KDFSalt nie ukrywa wymogu, bo wyzerowany bit mieszka w /P, a /P jest uwierzytelniane przez /Perms. Odszyfrowywanie AESV4 w PDFium Component traktuje więc wyzerowany bit 13 albo leftover /KDFSalt bez /AuthCode jako okaleczony MAC i pada, a gdy ochrona integralności jest wyłączona, writer ustawia bit 13, więc jego wyjście nigdy nie ogłasza MAC-a, którego nie ma. Hierarchia kluczy i zakres bajtów za tym tokenem należą do szyfrowania AES-GCM i MAC-a ISO/TS 32004 w PDF 2.0, a iterowany hash hasła, który odblokowuje klucz pliku, zanim cokolwiek z tego ruszy, omawia hash hasła R6 i jego liczba rund

Permissions czy UserPermissions: którą powinno czytać twoje UI?

Czytaj Permissions, by rozstrzygnąć, co może zrobić bieżąca sesja, i UserPermissions, by raportować, co plik przyznaje zwykłemu odbiorcy. TPdf.Permissions owija FPDF_GetDocPermissions z PDFium i opisuje dostęp przyznany temu otwartemu dokumentowi, co zależy od hasła, które go odblokowało. TPdf.UserPermissions owija FPDF_GetDocUserPermissions i dalej raportuje przydział dostępu użytkownika z /P, niezależnie od tego, które hasło zostało użyte, więc oba mogą się różnić, gdy plik otworzyło hasło owner. Oba są LongWord; GrantedPermissions i UserGrantedPermissions dekodują je w zbiory TPdfPermissions, a HasPermission i UserHasPermission testują pojedynczy człon. SecurityHandlerRevision mówi ci, czy plik w ogóle ma handler bezpieczeństwa. Procedura poniżej wypisuje oba całkowite widoki surowego słowa obok zdekodowanego zbioru — szybki sposób, by zobaczyć, czemu plik "nie chce się drukować"

uses
  System.SysUtils, System.TypInfo, PDFium;

procedure DumpPermissions(const FileName, Password: string);
var
  Pdf: TPdf;
  Mask: LongWord;
  Granted: TPdfPermissions;
  P: TPdfPermission;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Password := Password;   // ustaw przed Active
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.CreateFmt('Could not open %s', [FileName]);

    if Pdf.SecurityHandlerRevision <= 0 then
    begin
      Writeln('Not encrypted: no /P to decode');
      Exit;
    end;

    Mask := Pdf.UserPermissions;
    Granted := Pdf.UserGrantedPermissions;
    Writeln(Format('R%d  /P as Integer %d, as Cardinal %u ($%.8x)',
      [Pdf.SecurityHandlerRevision, Integer(Mask), Mask, Mask]));

    for P := Low(TPdfPermission) to High(TPdfPermission) do
      Writeln(Format('  %-22s %s',
        [GetEnumName(TypeInfo(TPdfPermission), Ord(P)),
         BoolToStr(P in Granted, True)]));

    if not Pdf.HasPermission(permCopy) then
      Writeln('  this session may not copy text');
  finally
    Pdf.Free;
  end;
end;

// Oczekiwane dla pliku zapisanego wcześniej:
//   R6  /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
//   permPrint True, permModify False, permCopy False, ...

Egzekwowanie należy do aplikacji gospodarza

PDFium renderuje, ekstrahuje tekst i rastruje strony, cokolwiek mówią flagi. Silnik raportuje uprawnienia i zostawia politykę wołającemu. Ktokolwiek trzyma hasło user, trzyma odszyfrowaną zawartość — przy pustym haśle user to każdy — więc poufność może pochodzić wyłącznie z hasła user, którego czytnik nie ma. Włóż kontrolę do każdego handlera komendy (kopiuj, eksportuj, usuń stronę, drukuj), zamiast chować przyciski paska narzędzi, bo skróty, menu kontekstowe i automatyzacja wszystko omijają pasek — to samo warstwowanie użyte w bezpiecznym okienku podglądu PDF dla aplikacji Delphi. Komenda druku, na przykład, testuje permPrint, zanim cokolwiek trafi do kolejki drukarki

W pliku AES-256 -1084 w postaci jawnej nie dowodzi niczego, dopóki pierwsze cztery odszyfrowane bajty /Perms nie odczytają się jako C4 FB FF FF. To porównanie bajt w bajt biegnie wewnątrz kodu AESV4, PDF MAC i zaszyfrowanych zapisów przyrostowych PDFium Component