기술 문서

PDF 권한 플래그, /P 정수, /Perms 사본

모든 것을 허용하려던 마스크 $FFFFFFF0로 보고서를 암호화했는데 Acrobat의 보안 탭이 인쇄와 문서 변경을 허용 안 됨으로 나열합니다. /P는 비트 1부터 매겨진 부호 없는 32비트 플래그 워드이므로 그 마스크는 인쇄(비트 3)와 수정(비트 4)을 클리어합니다. 모두 허용하는 값은 $FFFFFFFC이며 PDFium Component의 TPdfEncryptOptions 시작점이 바로 그것입니다

/P 플래그 워드는 어떻게 배열되어 있는가

의미를 지니는 자리는 여덟뿐이고 나머지는 예약이며, 셈은 1부터 시작합니다. ISO 32000-1 §7.6.3.2는 P가 "부호 없는 32비트 수량으로 해석되어야 한다"고 말하고, 낮은 쪽 끝의 1부터 높은 쪽 끝의 32까지 매기며, 표준을 따르는 reader에게 3, 4, 5, 6, 9, 10, 11, 12가 아닌 모든 비트를 무시하라고 이릅니다. ISO 32000-2 §7.6.4.2는 Table 22를 바로잡고 예약 자리를 밝힙니다. 비트 1–2는 0이어야 하고 비트 7–8은 1이어야 하며 리비전 3 이상에서는 비트 13–32가 1이어야 합니다. PDF 2.0은 또한 비트 10을 은퇴시킵니다. reader들은 이제 그것을 무시하고 writer들은 오래된 reader를 위해 언제나 1로 set합니다. 접근성 면제는 비트 5로 옮겨가서, 콘텐츠가 보조 기술로 향할 때 reader는 복사가 허용된 것처럼 동작해야 합니다. 아래의 모든 행을 하나의 공식이 덮습니다. 비트 n의 마스크는 1 shl (n - 1)이고 TPdfPermission도 같은 방식으로 워드에 대응합니다

비트마스크의미(리비전 3 이후)TPdfPermission
1–2$00000003예약, 0이어야 함none
3$00000004인쇄, 품질은 비트 12에 따름permPrint
4$00000008내용 변경, 비트 6, 9, 11의 조작은 제외permModify
5$00000010텍스트와 그래픽 복사 또는 추출permCopy
6$00000020주석 추가·수정, 필드 채우기. 비트 4와 함께라면 필드 생성permAnnotate
7–8$000000C0예약, 1이어야 함none
9$00000100비트 6이 꺼져 있어도 서명 필드 포함 기존 폼 필드 채우기permFillForms
10$00000200접근성 추출. PDF 2.0에서는 미사용, 언제나 1로 기록permAccessibility
11$00000400조합: 페이지 삽입·회전·삭제, 아웃라인 항목과 썸네일 생성permAssemble
12$00000800충실하고 고품질인 표현으로 인쇄permPrintHighQuality
13–32$FFFFF000예약, 1이어야 함(ISO/TS 32004는 비트 13을 할당, 아래 참조)none

$FFFFFFF0 마스크는 "낮은 비트들은 예약"이라는 말을 읽고 안전하게 하려고 니블 통째로 클리어한 데서 옵니다. 예약 영역은 비트 1–2, 마스크 $3뿐이었습니다. $F를 클리어하면 $4와 $8, 곧 인쇄와 수정마저 지워지고 플래그를 존중하는 뷰어는 둘 다 거부합니다. 거울상의 실수는 마스크를 0에서 위로 조립하는 것입니다. $4, $10, $800을 OR하면 비트 7–8과 13–32가 꺼진 작은 양수 /P가 나옵니다. 대부분의 reader는 명세가 예약 비트를 무시하라고 하므로 아무 말 없이 그 파일을 열지만, 그 값은 리비전 3 이상에서는 표준에 맞지 않고 엄격한 검사기가 불평할 자격이 있습니다. 세 번째 변형은 비트를 0부터 세는 코드에서 나옵니다. "비트 3"이 $8이 되어 프로그램은 인쇄를 뜻하려 했으면서 수정을 허용합니다. 이들 중 어느 것도 저장 시점에 오류를 내지 않습니다. 권한은 그저 저자가 의도한 것과 다르게 나올 뿐입니다

Delphi의 PDFium Component를 위한 PDF /P 권한 플래그 워드 배치: 예약 비트 1-2, 7-8, 13-32, permPrint와 permModify 같은 TPdfPermission 비트, 마스크 $FFFFFFFC와 $FFFFFFF0
$FFFFFFF0으로 낮은 니블을 통째로 클리어하면 인쇄와 수정도 함께 사라지고, 0부터 마스크를 올려 만들면 리비전 3 이상의 예약된 1들이 꺼진 채로 남습니다

/P -4와 /P 4294967292는 같은 값인가?

예. 같은 32비트, 두 표기입니다. PDF 정수 객체는 부호가 있고 Table 22 바로 위의 노트는 예약된 높은 비트들이 1이므로 정수가 음수로 기록된다고 짚습니다. -4는 리비전 3 이상의 관습적인 모두 허용 값입니다. 값을 부호 없는 변수에 보관하는 producer들은 대신 4294967292를 쓰고, 실제 파일에는 두 표기가 모두 등장합니다. 어느 읽기가 옳은지는 명세가 자기 키 유도에서 정리합니다. ISO 32000-1 §7.6.3.3의 Algorithm 2 단계 (d)는 P를 32비트 부호 없는 이진수로 바꾸고 네 바이트를 낮은 바이트부터 MD5에 넘기므로, 리비전 2부터 4까지는 -3904와 4294963392가 같은 파일 키를 냅니다. /P를 부호 있는 32비트 Integer에 저장하는 파서는 부호 없는 표기를 오버플로로 거부합니다. 변수를 Cardinal로 바꾸면 이번에는 음수 표기가 실패하고, 토큰 텍스트를 저장된 값과 비교하는 코드는 하나의 문서를 둘로 칩니다. 토큰을 Int64로 읽고, Low(Integer)부터 High(Cardinal)까지 무엇이든 받아들이고, 비교 전에 32비트로 마스크하세요. PDFium Component의 자체 복호화 코드는 리비전 2부터 7까지의 /P를 그렇게 읽고, writer는 명세 노트가 보여 주는 부호 있는 형태를 냅니다. 스니펫의 예약 비트 보고는 비트 14에서 멈춥니다. 클리어된 비트 13이 ISO/TS 32004가 PDF MAC을 지닌 파일에 찍는 표식이기 때문입니다

uses
  System.SysUtils;

// 당신의 사전 파서가 내놓은 /P 토큰 텍스트를 정규화합니다
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; ';
  // 비트 13($1000)은 뺐습니다: ISO/TS 32004는 PDF MAC을 요구하려고 이를 클리어합니다
  if (Mask and $FFFFE000) <> $FFFFE000 then
    Result := Result + 'bits 14-32 not all set; ';
end;

// 예상: 두 호출 모두 $FFFFFFFC와 빈 예약 비트 보고를 냅니다
//   TryNormalizePermissions('-4', M1);
//   TryNormalizePermissions('4294967292', M2);
PDFium Component처럼 Delphi에서 PDF /P 값을 정규화: /P -4와 /P 4294967292를 Int64로 읽고 Low(Integer)부터 High(Cardinal)까지 범위를 검사해 Cardinal $FFFFFFFC로 마스크합니다
부호 있는 Integer 파서와 Cardinal 파서는 각각 /P의 합법 표기 하나씩을 거부하며, Int64로 읽은 다음 32비트로 마스크하면 두 표기가 하나의 값으로 모입니다

TPdfPermissions 집합에서 마스크 빚기

모든 예약 비트를 요구된 값으로 시작하고 허용하는 것만 OR하세요. 잊힌 권한은 언제나 거부로만 끝날 수 있습니다. FPdfEncrypt 유닛의 TPdfEncryptOptions.Default는 이미 $FFFFFFFC를 지니고 있습니다. 모든 것을 허용하며, 빠른 제한에는 그것에서 비트 한두 개를 클리어하는 것으로 충분합니다. 수신자가 무엇을 할 수 있는지 나열하는 정책은 집합으로 읽기 좋고, 아래 상수 배열은 TPdf가 워드를 디코딩에 쓰는 비트 위치와 나란합니다. 호출자가 무엇을 넘기든 비트 10을 강제로 켜세요. ISO 32000-2는 writer에게 언제나 set하라고 이르고 PDF 2.0 reader는 어차피 무시하며, 꺼진 채로 두면 오래된 소프트웨어에서 화면 낭독기를 막을 위험만 감수하게 됩니다

uses
  System.SysUtils, PDFium, FPdfEncrypt;

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

  // 비트 7-8과 13-32가 켜짐; 비트 1-2와 모든 권한 비트는 꺼짐
  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
    Opts.UserPassword := '';              // 누구나 파일을 열 수 있습니다
    Opts.OwnerPassword := OwnerPassword;  // 권한 집합을 지킵니다
    Opts.Revision := erR6;
    Opts.Permissions := PermissionsToMask(
      [permPrint, permPrintHighQuality, permFillForms]);

    // 예상: /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;

예상: 출력의 Encrypt 사전이 /P -1084를 나르고, 플래그를 존중하는 뷰어는 전체 품질 인쇄와 폼 채우기를 제공하면서 콘텐츠 편집, 복사, 코멘트, 페이지 조합을 회색으로 둡니다. 비트 13은 writer가 당신을 위해 관리하는 자리 하나입니다. EnableIntegrityProtection을 False로 두면 SaveAsEncrypted는 마스크가 무엇이라 말든 비트 13을 set하고, True로 두면 writer는 /P와 /Perms 양쪽에서 비트 13을 클리어합니다. 나머지 예약 비트는 당신의 책임으로 남습니다. 그래서 $FFFFF0C0 시작점이 있습니다. 예제는 erR6를 고릅니다. R6은 ISO 32000-2가 표준화하는 AES-256 리비전이고 기본 레코드는 erR5를 지니기 때문입니다

/Perms, AES-256 파일에서 /P의 암호화 사본

리비전 2부터 4까지는 권한 워드가 키에 용접되어 있습니다. /P의 비트 하나만 바꿔도 MD5 입력이 바뀌고 유도된 키가 틀려 아무것도 복호화되지 않습니다. AES-256 파일은 그 연결을 끊습니다. ISO 32000-2 §7.6.4.3.3의 Algorithm 2.A는 /UE나 /OE에서 파일 암호화 키를 감싸 풀고 P는 그 유도에 관여하지 않으므로, 그 자체로는 평문 /P가 마음대로 편집될 수 있습니다. /Perms가 그 간극을 닫습니다. Algorithm 10(§7.6.4.4.9)은 16바이트 블록을 채워 파일 키로 AES-256 ECB 모드로 암호화합니다:

  • 바이트 0–3: 권한 워드, 낮은 바이트부터, 바이트 4–7이 64비트로 늘여 줍니다. 그들은 모두 $FF입니다
  • 바이트 8: ASCII T 또는 F, /EncryptMetadata와 맞춤
  • 바이트 9–11: ASCII 표식 adb
  • 바이트 12–15: reader가 무시하는 무작위 데이터

Algorithm 2.A의 단계 (f)는 블록을 복호화하고 adb를 검증하며 바이트 0–3이 P와 맞기를 요구합니다 — shall이지 should가 아닙니다. Algorithm 13(§7.6.4.4.12)은 표식과 권한 검사를 더 부드러운 should로 반복하고 하나를 더 얹습니다. 바이트 8을 EncryptMetadata와 맞추는 것입니다. PDFium Component는 자기 코드가 암호화 계층을 여는 곳마다 엄격하게 읽습니다. AESV4 복호화, PDF MAC 검증, 암호화된 파일로의 증분 저장이 그것입니다. 사용자나 소유자 암호가 파일 키를 연 뒤 그 코드는 네 개의 권한 바이트를 모두 정규화된 /P와 비교하고, $FF 필러와 T/F 바이트, 표식을 검사하며, 하나라도 다르면 "PDF encryption permissions validation failed"로 로드에 실패합니다. 어긋남은 누군가가 암호화 뒤에 사전을 편집했고 어느 사본도 곧이곧대로 받을 수 없다는 뜻이니까요

PDFium Component의 AES-256 /Perms 블록: 낮은 바이트부터의 /P 권한 워드, $FF 필러, /EncryptMetadata의 T 또는 F 바이트, adb 표식. Algorithm 10이 암호화하고 Algorithm 2.A와 13 아래에서 검증됩니다
AES-256 파일 키가 더 이상 /P에 의존하지 않으므로, 복호화된 /Perms 바이트가 평문 권한 워드가 암호화 뒤에 편집되지 않았음을 증명하는 것입니다

EncryptMetadata 비교는 당신이 쓰는 어떤 reader에서도 신경 쓸 가치가 있습니다. Pos('/EncryptMetadata false', Dict) > 0 같은 지름길은 공백 둘로 띄어진 /EncryptMetadata false, 키와 값 사이의 줄바꿈, 그 사이에 낀 주석을 놓칩니다. 모두 합법적인 PDF 문법입니다. reader는 그러다 기본값 true를 가정하고 완벽하게 유효한 /Perms에서 F를 발견해 파일을 거부합니다. 유효한 파일이 거부됩니다. 키와 값을 토큰으로 파싱하고, 불리언이 아닌 값은 기본값으로 대체하기보다 malformed로 취급하세요

ISO/TS 32004는 비트 13에 직업을 줍니다. 클리어되어 있으면 문서가 reader가 검사해야 하는 PDF MAC을 지닌다는 뜻입니다. MAC 토큰은 트레일러의 /AuthCode 사전에 앉으며 그것은 암호화되지 않으므로 /AuthCode만 찾는 검사는 그것이 지워진 파일이 무결성 보호를 받은 적이 없다고 결론내립니다. /KDFSalt까지 지워도 요구는 숨지 않습니다. 클리어된 비트는 /P 안에 살고 /P는 /Perms가 인증하기 때문입니다. 따라서 PDFium Component의 AESV4 복호화는 /AuthCode 없는 클리어된 비트 13이나 남은 /KDFSalt를 벗겨진 MAC으로 치고 실패하며, 무결성 보호가 꺼져 있을 때 writer는 비트 13을 set해 자기 출력이 지니지 않은 MAC을 결코 알리지 않게 합니다. 그 토큰 뒤의 키 계층과 바이트 범위는 PDF 2.0의 AES-GCM 암호화와 ISO/TS 32004 MAC에 속하고, 이 모든 것에 앞서 파일 키를 여는 반복된 암호 해시는 R6 암호 해시와 그 라운드 수에서 다룹니다

Permissions인가 UserPermissions인가, UI는 어느 것을 읽어야 하나

현재 세션이 무엇을 할 수 있는지는 Permissions로 정하고, 파일이 평범한 수신자에게 허용하는 것은 UserPermissions로 보고하세요. TPdf.Permissions는 PDFium의 FPDF_GetDocPermissions를 감싸며 이렇게 열린 문서가 부여받은 접근, 곧 그것을 연 암호에 달린 것을 기술합니다. TPdf.UserPermissions는 FPDF_GetDocUserPermissions를 감싸고 어떤 암호를 썼든 /P의 사용자 접근 부여를 계속 보고하므로, 소유자 암호가 파일을 열었을 때 둘은 달라질 수 있습니다. 둘 다 LongWord입니다. GrantedPermissions와 UserGrantedPermissions는 그것들을 TPdfPermissions 집합으로 디코딩하고 HasPermission과 UserHasPermission은 멤버 하나를 검사합니다. SecurityHandlerRevision은 파일에 보안 핸들러가 아예 있는지를 알려 줍니다. 아래 루틴은 원본 워드의 정수 뷰 둘을 디코딩된 집합 곁에 인쇄합니다. 파일이 "인쇄되지 않는" 이유를 빨리 보는 방법입니다

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;   // 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;

// 앞서 쓴 파일에 대한 예상:
//   R6  /P as Integer -1084, as Cardinal 4294966212 ($FFFFFBC4)
//   permPrint True, permModify False, permCopy False, ...

집행은 호스트 애플리케이션의 몫입니다

PDFium은 플래그가 무엇이라 말든 페이지를 렌더링하고 텍스트를 추출하고 래스터화합니다. 엔진은 권한을 보고하고 정책은 호출자에게 맡깁니다. 사용자 암호를 쥔 자는 복호화된 콘텐츠를 쥡니다. 빈 사용자 암호라면 그것은 모든 사람입니다. 따라서 기밀성은 reader가 지니지 않은 사용자 암호에서만 옵니다. 검사를 툴바 버튼 숨기기보다 각 명령 핸들러(복사, 내보내기, 페이지 삭제, 인쇄) 안에 넣으세요. 단축키와 콘텍스트 메뉴, 자동화가 전부 툴바를 우회합니다. Delphi 앱을 위한 안전한 PDF 미리보기 창이 쓰는 것과 같은 계층화입니다. 가령 인쇄 명령은 무엇이든 스풀하기 전에 permPrint를 검사합니다

AES-256 파일에서 평문의 -1084는 /Perms의 복호화된 첫 네 바이트가 C4 FB FF FF로 읽히기 전까지는 아무것도 증명하지 않습니다. 그 바이트 단위 비교는 PDFium Component의 AESV4, PDF MAC, 암호화된 증분 저장 코드 안에서 돌아갑니다